├── pyproject.toml ├── src └── utils │ ├── pl │ ├── assets │ │ ├── abbreviations.json │ │ ├── default.mplstyle │ │ └── named_colors.json │ ├── plot_custom.py │ ├── plot_ternary.py │ ├── plot_overlaps.py │ ├── plot_embedding.py │ └── plot_trends.py │ ├── tl │ ├── clinical_associations.py │ ├── state_transitions.py │ └── transfer_labels.py │ └── pp │ ├── assets │ └── ribosomal_genes.gmt │ └── preprocess.py ├── notebooks ├── requirements.py ├── download_data.ipynb └── Figure_4.ipynb ├── .gitignore └── README.md /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=58.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "my_project" 7 | version = "1.0.0" 8 | description = "My project description" 9 | authors = ["Andrew Moorman "] 10 | license = "MIT" 11 | 12 | keywords = [] 13 | classifiers = [] 14 | 15 | requires-python = ">=3.8" 16 | dependencies = [ 17 | "scanpy", 18 | "matplotlib", 19 | "pandas", 20 | "numpy", 21 | "seaborn", 22 | "openpyxl", 23 | "scipy", 24 | "scikit-misc", 25 | "PhenoGraph", 26 | "magic-impute", 27 | "python-ternary", 28 | "colorsys", 29 | "boto3", 30 | ] 31 | -------------------------------------------------------------------------------- /src/utils/pl/assets/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Absorptive Intestine": "Abs", 3 | "EMT": "EMT", 4 | "Injury Repair": "Inj", 5 | "Osteoblast": "Osteo", 6 | "Squamous": "Squa", 7 | "Neuroendocrine": "Neuro", 8 | "Endoderm Development": "Endo", 9 | "Tumor ISC-like": "ISC", 10 | "Secretory Intestine": "Sec", 11 | "Intestine": "Int", 12 | "Metastasis": "Metastasis", 13 | "Primary Tumor": "Primary", 14 | "ISC": "ISC", 15 | "Secretory Precursor": "Pre-Sec.", 16 | "Absorptive Precursor": "Pre-Abs.", 17 | "Goblet": "Goblet", 18 | "Enterocytes": "Enterocytes", 19 | "Enterocytes (BEST4+)": "BEST4+", 20 | "Tuft": "Tuft", 21 | "Enteroendocrine": "Enteroendocrine" 22 | } -------------------------------------------------------------------------------- /src/utils/pl/assets/default.mplstyle: -------------------------------------------------------------------------------- 1 | figure.dpi : 300 2 | font.family : "Arial" 3 | mathtext.default : "default" 4 | xtick.labelsize : 7 5 | ytick.labelsize : 7 6 | axes.labelsize : 9 7 | axes.titlesize : 9 8 | axes.facecolor : "white" 9 | xtick.direction : "out" 10 | ytick.direction : "out" 11 | image.cmap : "viridis" 12 | lines.linewidth : 0.75 13 | axes.spines.right : False 14 | axes.spines.top : False 15 | legend.borderaxespad : 0.25 16 | legend.borderpad : 0.25 17 | legend.fontsize : 7 18 | legend.title_fontsize : 7 19 | legend.handleheight : 0.5 20 | legend.handlelength : 1.0 21 | legend.markerscale : 0.5 22 | patch.linewidth : 0.75 23 | axes.prop_cycle : cycler("color", ["#F04437", "#E81F64", "#903E97", "#65499E", "#4356A5", "#478FCC", "#34A4DD", "#00BCD4", "#009889", "#4BB04F", "#8BC34C", "#CCDA3A", "#FCED3A", "#FFC10E", "#F8991D", "#F1592C", "#7A5649", "#9F9E9E", "#607F8C",]) -------------------------------------------------------------------------------- /notebooks/requirements.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file contains standardized imports and variables used throughout the 3 | analysis notebooks for "Progressive Plasticity in Colorectal Cancer Metastasis" 4 | by Moorman et al (DOI: ) 5 | ''' 6 | # Packages used throughout 7 | import scanpy as sc 8 | import anndata 9 | import numpy as np 10 | import scipy as sp 11 | import pandas as pd 12 | from matplotlib import pyplot as plt 13 | import seaborn as sns 14 | import os 15 | import sys 16 | import json 17 | import tqdm 18 | import pickle 19 | from pathlib import Path 20 | 21 | # Local modules used throughout 22 | module_path = os.path.abspath('../src') 23 | if module_path not in sys.path: 24 | sys.path.append(module_path) 25 | 26 | # Standard variables referenced throughout 27 | data_dir = Path(f"{os.getcwd()}/../data") 28 | media_dir = Path(f"{os.getcwd()}/../media") 29 | 30 | # Plotting styles used throughout 31 | module_path = os.path.abspath('../src') 32 | stylesheet = f'{module_path}/utils/pl/assets/default.mplstyle' 33 | plt.style.use(stylesheet) 34 | with open(f'{module_path}/utils/pl/assets/named_colors.json', 'r') as f: 35 | named_colors = json.load(f) 36 | with open(f'{module_path}/utils/pl/assets/abbreviations.json', 'r') as f: 37 | abbreviations = json.load(f) -------------------------------------------------------------------------------- /src/utils/tl/clinical_associations.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import scipy as sp 3 | from typing import List, Tuple 4 | 5 | 6 | def get_binary_association( 7 | enrichments: pd.DataFrame, 8 | annotations: pd.DataFrame, 9 | module: str, 10 | column: str, 11 | negative_vals: List, 12 | ): 13 | idx = enrichments[module].dropna().index 14 | idx = idx.intersection(annotations[column].dropna().index) 15 | mask = annotations.loc[idx, column].isin(negative_vals) 16 | r, p = sp.stats.ranksums( 17 | enrichments.loc[idx[~mask], module], 18 | enrichments.loc[idx[mask], module], 19 | ) 20 | return r, p 21 | 22 | 23 | def get_continuous_association( 24 | enrichments: pd.DataFrame, 25 | annotations: pd.DataFrame, 26 | module: str, 27 | column: str, 28 | ): 29 | idx = enrichments[module].dropna().index 30 | idx = idx.intersection(annotations[column].dropna().index) 31 | r, p = sp.stats.pearsonr(enrichments[module]) 32 | return r, p 33 | 34 | 35 | def get_associations( 36 | enrichments: pd.DataFrame, 37 | annotations: pd.DataFrame, 38 | modules: List, 39 | binary_columns: dict, 40 | continuous_columns: List[str], 41 | ): 42 | associations = pd.DataFrame( 43 | index=modules, 44 | columns=list(binary_columns.keys()) + continuous_columns, 45 | dtype=float, 46 | ) 47 | pvals = associations.copy() 48 | 49 | for module in modules: 50 | for column in continuous_columns: 51 | r, p = get_continuous_association( 52 | enrichments, annotations, module, column, 53 | ) 54 | associations.loc[module, column] = r 55 | pvals.loc[module, column] = p 56 | 57 | for column, negative in binary_columns.items(): 58 | if type(negative) != list: negative = [negative] 59 | r, p = get_binary_association( 60 | enrichments, annotations, module, column, negative, 61 | ) 62 | associations.loc[module, column] = r 63 | pvals.loc[module, column] = p 64 | 65 | return associations, pvals 66 | -------------------------------------------------------------------------------- /src/utils/pl/assets/named_colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "Module Absorptive Intestine Score": "#244B9E", 3 | "Module EMT Score": "#924599", 4 | "Module Injury Repair Score": "#D357A1", 5 | "Module Osteoblast Score": "#831518", 6 | "Module Squamous Score": "#E61F32", 7 | "Module Neuroendocrine Score": "#E25E27", 8 | "Module Endoderm Development Score": "#FAA71B", 9 | "Module Tumor ISC-like Score": "#099848", 10 | "Module Secretory Intestine Score": "#009DCE", 11 | "Module Intestine Score": "#0077A9", 12 | "Absorptive Intestine": "#244B9E", 13 | "EMT": "#924599", 14 | "Injury Repair": "#D357A1", 15 | "Osteoblast": "#831518", 16 | "Squamous": "#E61F32", 17 | "Neuroendocrine": "#E25E27", 18 | "Endoderm Development": "#FAA71B", 19 | "Tumor ISC-like": "#099848", 20 | "Secretory Intestine": "#009DCE", 21 | "Intestine": "#0077A9", 22 | "Absorptive Intestine Module Gene Score": "#244B9E", 23 | "EMT Module Gene Score": "#924599", 24 | "Injury Repair Module Gene Score": "#D357A1", 25 | "Osteoblast Module Gene Score": "#831518", 26 | "Squamous Module Gene Score": "#E61F32", 27 | "Neuroendocrine Module Gene Score": "#E25E27", 28 | "Endoderm Development Module Gene Score": "#FAA71B", 29 | "Tumor ISC-like Module Gene Score": "#099848", 30 | "Secretory Intestine Module Gene Score": "#009DCE", 31 | "Intestine Module Gene Score": "#0077A9", 32 | "Fetal": "#771434", 33 | "Fetal, Conserved": "#771434", 34 | "Fetal, Conserved (Normalized)": "#771434", 35 | "palantir_neuroendocrine_branch_probability": "#E25E27", 36 | "palantir_squamous_branch_probability": "#E61F32", 37 | "Primary": "#EBA131", 38 | "Primary Tumor": "#EBA131", 39 | "Metastasis": "#625793", 40 | "ISC": "#6BBE45", 41 | "Secretory Precursor": "#5587A2", 42 | "Goblet": "#97D5E0", 43 | "Absorptive Precursor": "#F99B86", 44 | "Enterocytes": "#C04D92", 45 | "Enterocytes (BEST4+)": "#7F2631", 46 | "Enteroendocrine": "#EE5428", 47 | "Tuft": "#C05950", 48 | "Treatment-Naive": "#448C82", 49 | "Treated": "#C35E34", 50 | "Canonical": "#35739B", 51 | "Non-Canonical": "#AE3138" 52 | } -------------------------------------------------------------------------------- /src/utils/tl/state_transitions.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import scanpy as sc 3 | import itertools 4 | from typing import List 5 | import numpy as np 6 | import scipy as sp 7 | import tqdm 8 | 9 | 10 | def get_feature_overlaps( 11 | adata: sc.AnnData, 12 | feature_columns: List[str], 13 | qtl_threshold: float = 0.75, 14 | ) -> pd.DataFrame: 15 | # Count co-occurrence of features in cells 16 | thresholds = adata.obs[feature_columns].quantile(qtl_threshold) 17 | counts = adata.obs[feature_columns].ge(thresholds) 18 | overlaps = pd.DataFrame(dtype=float) 19 | for f1 in feature_columns: 20 | for f2 in feature_columns: 21 | overlaps.loc[f1, f2] = counts[[f1, f2]].all(axis=1).sum() 22 | # Frequencies of co-occurrence of features in cells 23 | overlaps = overlaps.divide(overlaps.max(0), 0) 24 | return overlaps 25 | 26 | 27 | def test_ratios( 28 | df: pd.DataFrame, 29 | groupby: List[str], 30 | n_iters: int = 1000, 31 | ): 32 | # Reference fractions 33 | fracs = df.groupby(groupby).mean()['High'] 34 | ratios = fracs.xs('Metastasis', level=1) / fracs.xs('Primary', level=1) 35 | mask = ~ratios.isin([np.inf, 0]) 36 | ratios = ratios.replace(np.inf, ratios[mask].max()) 37 | ratios = ratios.replace(0, ratios[mask].min()) 38 | 39 | # Null fractions from random shuffling 40 | null_ratios = pd.DataFrame( 41 | index=ratios.index, 42 | columns=range(n_iters), 43 | dtype=float, 44 | ) 45 | 46 | def shuffle(group): 47 | idx = group.index 48 | group = group.sample(frac=1) 49 | group.index = idx 50 | return group 51 | 52 | for i in tqdm.tqdm(null_ratios.columns): 53 | null_df = df.copy() 54 | gb = null_df.groupby('Patient', group_keys=False) 55 | null_df['High'] = gb['High'].apply(shuffle) 56 | null_fracs = null_df.groupby(groupby).mean()['High'] 57 | null_ratios[i] = null_fracs.xs('Metastasis', level=1) 58 | null_ratios[i] /= null_fracs.xs('Primary', level=1) 59 | 60 | # Remove infinities for stats calculation 61 | mask = ~null_ratios.isin([np.inf, 0]) 62 | null_ratios = null_ratios.replace(np.inf, null_ratios[mask].max()) 63 | null_ratios = null_ratios.replace(0, null_ratios[mask].min()) 64 | r, p = sp.stats.ranksums( 65 | np.log10(ratios.values.flatten()), 66 | np.log10(null_ratios.values.flatten()), 67 | alternative='greater', 68 | ) 69 | 70 | return r, p 71 | -------------------------------------------------------------------------------- /src/utils/pl/plot_custom.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from scipy.stats import gaussian_kde 4 | from sklearn.preprocessing import MinMaxScaler 5 | import sys 6 | import os 7 | 8 | 9 | # Class to suppress output from PhenoGraph 10 | class HiddenPrints: 11 | def __enter__(self): 12 | self._original_stdout = sys.stdout 13 | sys.stdout = open(os.devnull, "w") 14 | 15 | def __exit__(self, exc_type, exc_val, exc_tb): 16 | sys.stdout.close() 17 | sys.stdout = self._original_stdout 18 | 19 | 20 | def quartile_to_level(data, quantile): 21 | """Return data levels corresponding to quantile cuts of mass.""" 22 | isoprop = np.asarray(quantile) 23 | values = np.ravel(data) 24 | sorted_values = np.sort(values)[::-1] 25 | normalized_values = np.cumsum(sorted_values) / values.sum() 26 | idx = np.searchsorted(normalized_values, 1 - isoprop) 27 | levels = np.take(sorted_values, idx, mode="clip") 28 | return levels 29 | 30 | 31 | def get_kde( 32 | data, 33 | grid_size=500, 34 | min_q=0.0, 35 | **kwargs 36 | ): 37 | kernel = gaussian_kde(data, **kwargs) 38 | positions = np.linspace(data.min(), data.max(), grid_size) 39 | estimate = kernel(positions) 40 | level = quartile_to_level(estimate, min_q) 41 | mask = estimate>=level 42 | return positions[mask], estimate[mask]-level 43 | 44 | 45 | def plot_violin( 46 | data, x, y, 47 | palette, 48 | ax, 49 | bw_adjust=1, 50 | grid_size=100, 51 | h=2, 52 | norm=True, 53 | min_q=0.01, 54 | lw=0.5, 55 | ): 56 | n_groups = data[x].nunique() 57 | x_ticks = np.linspace(0, -h * (n_groups - 1), n_groups) 58 | 59 | for (name, group), xpos in zip(data.sort_values(x).groupby(x), x_ticks): 60 | 61 | # Violin Plot 62 | n = group.shape[0] 63 | bw_method = group.shape[0] ** (-1/(5)) * bw_adjust 64 | pos, est = get_kde(group[y], min_q=min_q, bw_method=bw_method) 65 | if norm: 66 | est = MinMaxScaler().fit_transform(est.reshape(-1,1)).flatten() 67 | ax.fill_betweenx( 68 | pos, xpos - est, xpos + est, 69 | facecolor=palette[name], alpha=0.75, lw=0, 70 | zorder=1, 71 | ) 72 | ax.plot(xpos + est, pos, lw=lw, color="w", zorder=1) 73 | ax.plot(xpos - est, pos, lw=lw, color="w", zorder=1) 74 | # IQR Plot 75 | ymin, ymid, ymax = group[y].quantile([0.25, 0.5, 0.75]) 76 | ax.plot([xpos, xpos], [ymin, ymax], lw=0.5, color="black", alpha=0.75) 77 | ax.scatter([xpos], [ymid], s=2, color="black", alpha=0.9) 78 | 79 | ax.set_xticks(x_ticks) 80 | 81 | 82 | def plot_heatmap( 83 | data: pd.DataFrame, 84 | k: int = 35, 85 | ): 86 | pass 87 | -------------------------------------------------------------------------------- /notebooks/download_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 298, 6 | "id": "46d41c2d-35de-4e93-ad78-d2e372c35880", 7 | "metadata": { 8 | "execution": { 9 | "iopub.execute_input": "2024-10-29T17:20:47.654917Z", 10 | "iopub.status.busy": "2024-10-29T17:20:47.654596Z", 11 | "iopub.status.idle": "2024-10-29T17:20:47.681942Z", 12 | "shell.execute_reply": "2024-10-29T17:20:47.681451Z", 13 | "shell.execute_reply.started": "2024-10-29T17:20:47.654895Z" 14 | }, 15 | "tags": [] 16 | }, 17 | "outputs": [ 18 | { 19 | "name": "stdout", 20 | "output_type": "stream", 21 | "text": [ 22 | "The autoreload extension is already loaded. To reload it, use:\n", 23 | " %reload_ext autoreload\n" 24 | ] 25 | } 26 | ], 27 | "source": [ 28 | "%load_ext autoreload\n", 29 | "%autoreload 2" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 299, 35 | "id": "4e588df2-1dba-41b5-b9af-38fd7359cfa5", 36 | "metadata": { 37 | "execution": { 38 | "iopub.execute_input": "2024-10-29T17:20:47.833464Z", 39 | "iopub.status.busy": "2024-10-29T17:20:47.833074Z", 40 | "iopub.status.idle": "2024-10-29T17:20:47.857477Z", 41 | "shell.execute_reply": "2024-10-29T17:20:47.857016Z", 42 | "shell.execute_reply.started": "2024-10-29T17:20:47.833445Z" 43 | }, 44 | "tags": [] 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "from requirements import *\n", 49 | "import boto3" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "id": "9497daca-f0eb-4f70-ab62-04299e457023", 55 | "metadata": {}, 56 | "source": [ 57 | "## Download Source Data" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 297, 63 | "id": "554a293d-e26f-49dd-88f2-ffe73bd1081e", 64 | "metadata": { 65 | "execution": { 66 | "iopub.execute_input": "2024-10-29T17:20:17.716064Z", 67 | "iopub.status.busy": "2024-10-29T17:20:17.715748Z", 68 | "iopub.status.idle": "2024-10-29T17:20:18.646661Z", 69 | "shell.execute_reply": "2024-10-29T17:20:18.646169Z", 70 | "shell.execute_reply.started": "2024-10-29T17:20:17.716043Z" 71 | } 72 | }, 73 | "outputs": [ 74 | { 75 | "name": "stderr", 76 | "output_type": "stream", 77 | "text": [ 78 | "28it [00:00, 31.47it/s]\n" 79 | ] 80 | } 81 | ], 82 | "source": [ 83 | "s3 = boto3.client('s3')\n", 84 | "bucket_name = 'dp-lab-data-public'\n", 85 | "prefix = 'progressive-plasticity-crc-metastasis'\n", 86 | "data_bucket = boto3.resource('s3').Bucket(bucket_name)\n", 87 | "\n", 88 | "for obj in tqdm.tqdm(data_bucket.objects.filter(Prefix=prefix)):\n", 89 | " src_path = obj.key\n", 90 | " dst_path = obj.key.replace(prefix, str(data_dir))\n", 91 | " s3.download_file(bucket_name, src_path, dst_path)" 92 | ] 93 | } 94 | ], 95 | "metadata": { 96 | "kernelspec": { 97 | "display_name": "Python 3 (ipykernel)", 98 | "language": "python", 99 | "name": "python3" 100 | }, 101 | "language_info": { 102 | "codemirror_mode": { 103 | "name": "ipython", 104 | "version": 3 105 | }, 106 | "file_extension": ".py", 107 | "mimetype": "text/x-python", 108 | "name": "python", 109 | "nbconvert_exporter": "python", 110 | "pygments_lexer": "ipython3", 111 | "version": "3.8.1" 112 | } 113 | }, 114 | "nbformat": 4, 115 | "nbformat_minor": 5 116 | } 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | data* 165 | media* 166 | notebooks/_*.ipynb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Progressive Plasticity in Colorectal Cancer Metastasis 2 | 3 | This repository accompanies the study *"Progressive Plasticity During Colorectal Cancer Metastasis"* published in *Nature* (available [here](https://www.nature.com/articles/s41586-024-08150-0)), aiming to reproduce key figures and analyses from the paper. 4 | 5 | ### Project Overview 6 | The study investigates the progressive plasticity of cellular states during the metastasis of colorectal cancer. Using single-cell RNA sequencing data from primary tumors and metastases, we uncover dynamic cellular state transitions, highlighting specific lineage and state shifts associated with metastatic progression. 7 | 8 | ### Repository Structure 9 | 10 | - **data/**: Contains required data files: 11 | - **h5ads/**: AnnData files, such as `Tumor.h5ad`, `Epithelial.h5ad`, etc. 12 | - **tables/**: Supplementary tables in `.xlsx` format. 13 | - **other/**: Additional annotation and enrichment files. 14 | 15 | - **notebooks/**: Jupyter notebooks for data download, preprocessing, and reproducing figures: 16 | - `download_data.ipynb`: Guide for downloading data directly from AWS S3. 17 | - `Figure_X.ipynb`: Notebooks for reproducing figures in the paper. 18 | 19 | - **src/**: Source code modules organized by functionality, including utilities for data preprocessing (`pp`), plotting (`pl`), and label transfer or state analysis (`tl`). 20 | 21 | ### Data Access 22 | The processed H5AD data for reproducing this analysis is hosted on AWS S3 at: 23 | ``` 24 | s3://dp-lab-data-public/progressive-plasticity-crc-metastasis 25 | ``` 26 | You can download H5ADs directly using the following links: 27 | ``` 28 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/All.h5ad 29 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Epithelial.h5ad 30 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG146_Organoids.h5ad 31 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG146_Tumor.h5ad 32 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG146_Tumor_Mapping_Reference.h5ad 33 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG146_shPROX1_Knockdown.h5ad 34 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG150_Tumor.h5ad 35 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG182_Tumor.h5ad 36 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/KG183_Tumor.h5ad 37 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Non-Tumor_Epithelial.h5ad 38 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Tumor.h5ad 39 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Untreated_Epithelial.h5ad 40 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Wang_etal_Tumor.h5ad 41 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/h5ads/Wang_etal_s1231_Tumor.h5ad 42 | ``` 43 | Additionally, all data is available vis a single download link in .tar.gz format: 44 | ``` 45 | https://dp-lab-data-public.s3.us-east-1.amazonaws.com/progressive-plasticity-crc-metastasis/data.tar.gz 46 | ``` 47 | 48 | ### Installation 49 | 50 | To install the required dependencies, ensure you have Python 3.8 or higher and use the `pyproject.toml`: 51 | ```bash 52 | pip install . 53 | ``` 54 | 55 | For specific package versions, review the `pyproject.toml`. 56 | 57 | ### Quickstart 58 | 59 | 1. **Download Data**: Start with `download_data.ipynb` to load data files from AWS. 60 | 2. **Run Notebooks**: Open notebooks in the `notebooks` directory to generate individual figures. 61 | 62 | --- 63 | 64 | This README provides a basic overview of the repository's contents and is designed to support the reproducibility of key analyses and findings from the paper. 65 | -------------------------------------------------------------------------------- /src/utils/pl/plot_ternary.py: -------------------------------------------------------------------------------- 1 | import scipy as sp 2 | import numpy as np 3 | import pandas as pd 4 | import ternary 5 | from ternary.helpers import ( 6 | project_point, 7 | planar_to_coordinates, 8 | simplex_iterator 9 | ) 10 | from matplotlib.colors import cnames, to_rgb 11 | from matplotlib import collections, lines, pyplot as plt 12 | import seaborn as sns 13 | import colorsys 14 | from typing import List 15 | 16 | 17 | def ternary_kde( 18 | points, 19 | tax, 20 | n_levels=10, 21 | cmap="viridis", 22 | outline=False, 23 | bw_method='scott', 24 | ): 25 | # Project to 2D simplex 26 | simplex_points = np.apply_along_axis(project_point, 0, points) 27 | 28 | # Fit density model to 2D projection 29 | kde = sp.stats.gaussian_kde(simplex_points, bw_method=bw_method) 30 | 31 | # Evaluate density on triangular grid 32 | n = 100 33 | tri_grid = np.array(list(simplex_iterator(n))).T 34 | simplex_grid = np.apply_along_axis(project_point, 0, tri_grid)/n 35 | densities = kde(simplex_grid) 36 | levels = np.linspace( 37 | np.percentile(densities, 5), 38 | np.percentile(densities, 95), 39 | n_levels, 40 | ) 41 | tax.ax.tricontourf( 42 | simplex_grid[0], simplex_grid[1], 43 | densities, levels=levels, cmap=cmap, 44 | extend="both" 45 | ) 46 | if outline: 47 | tax.ax.tricontour( 48 | simplex_grid[0], simplex_grid[1], 49 | densities, levels=levels, 50 | colors=[[0,0,0,0.25]]+[[0,0,0,0.25]]*4, 51 | linewidths=[0.25]+[0.25]*4 52 | ) 53 | 54 | 55 | def format_tax( 56 | tax, 57 | labels, 58 | fontsize, 59 | tick_width, 60 | boundary_width, 61 | pad, 62 | ): 63 | tax.gridlines( 64 | color="gray", lw=tick_width, linestyle='--', alpha=0.5, multiple=0.5 65 | ) 66 | tax.ticks( 67 | axis='lbr', lw=tick_width, fontsize=fontsize, tick_formats='%.1f', 68 | offset=0.05, multiple=1.0, 69 | ) 70 | tax.boundary(linewidth=boundary_width, zorder=4) 71 | tax.clear_matplotlib_ticks() 72 | tax.get_axes().axis('off') 73 | 74 | 75 | def plot_ternary( 76 | points, 77 | cmap, 78 | titles, 79 | ax, 80 | n_pts: int = 1000, 81 | ): 82 | _, tax = ternary.figure(ax=ax) 83 | ternary_kde( 84 | points.T, 85 | tax, 86 | n_levels=9, 87 | cmap=cmap, 88 | outline=True, 89 | bw_method=0.3, 90 | ) 91 | idx = np.random.choice(points.shape[0], n_pts) 92 | tax.scatter(points[idx], s=0.5, lw=0, color="gray", alpha=0.33) 93 | format_tax( 94 | tax=tax, 95 | labels=titles, 96 | fontsize=8, 97 | tick_width=0.5, 98 | boundary_width=1, 99 | pad=2, 100 | ) 101 | 102 | 103 | def lighten_color(color, amount: float): 104 | # Lookup color in matplotlib named colors 105 | try: 106 | color = cnames[color] 107 | except: 108 | pass 109 | h, l, s = colorsys.rgb_to_hls(*to_rgb(color)) 110 | color = colorsys.hls_to_rgb(h, 1 - amount * (1 - l), s) 111 | return color 112 | 113 | 114 | def patch_violinplot(ax): 115 | children = ax.get_children() 116 | i = 0 117 | for n in range(0, len(children), 4): 118 | art = children[n: n+4] 119 | is_violin = len(art) == 4 120 | is_violin &= isinstance(art[0], collections.PolyCollection) 121 | is_violin &= all([isinstance(a, lines.Line2D) for a in art[1:]]) 122 | if is_violin: 123 | violin, q1, q2, q3 = art 124 | c = violin.get_facecolor() 125 | if i%2==1: c = lighten_color(c, 0.5) 126 | violin.set_facecolor(c) 127 | violin.set_edgecolor(c) 128 | violin.set_linewidth(0.1) 129 | q2.set_linestyle('solid') 130 | q2.set_linewidth(0.5) 131 | q2.set_solid_capstyle('butt') 132 | for q in [q1, q3]: 133 | q.set_alpha(0) 134 | i += 1 135 | 136 | 137 | def plot_kde( 138 | data: pd.DataFrame, 139 | row: str, 140 | row_order: List, 141 | **kwargs, 142 | ): 143 | # Update with default styles 144 | styles = dict( 145 | cut=0, 146 | common_norm=False, 147 | density_norm='width', 148 | width=0.75, 149 | gap=0, 150 | split=True, 151 | fill=True, 152 | linewidth=0.5, 153 | legend=False, 154 | inner='quart', 155 | ) 156 | kwargs.update(styles) 157 | 158 | # Plot KDEs 159 | grid = sns.FacetGrid( 160 | data, 161 | row=row, 162 | row_order=row_order, 163 | height=1.5, 164 | aspect=1.5, 165 | sharex=False, 166 | gridspec_kws=dict(hspace=0.5), 167 | despine=False, 168 | ) 169 | fig = grid.map_dataframe(sns.violinplot, **kwargs) 170 | 171 | # Formatting 172 | for ax in fig.axes.flat: 173 | patch_violinplot(ax) 174 | ax.set_title('') 175 | ax.set_xlim(0, 1) 176 | ax.yaxis.set_visible(False) 177 | ax.spines[['top', 'left', 'right']].set_visible(False) 178 | ax.set_xlabel(kwargs['x']) -------------------------------------------------------------------------------- /src/utils/pl/plot_overlaps.py: -------------------------------------------------------------------------------- 1 | import seaborn as sns 2 | import pandas as pd 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | from typing import List, Optional 6 | 7 | 8 | def plot_overlaps( 9 | overlaps: pd.DataFrame, 10 | feature_colors: dict, 11 | labels: List[str] = None, 12 | ): 13 | 14 | # Plot overlaps 15 | fig, ax = plt.subplots(1, 1, figsize=(2, 2)) 16 | sns.heatmap( 17 | overlaps, 18 | cmap='Purples', 19 | cbar_kws={'shrink': 0.2, 'aspect': 6}, 20 | vmin=0, 21 | vmax=1, 22 | ax=ax, 23 | ) 24 | n = overlaps.shape[0] 25 | ax.hlines(range(n+1), -1, n+0.1, color='w', lw=3, clip_on=False) 26 | 27 | # Adjust tick labels 28 | ax.tick_params(which='major', length=0, labelsize=6.5) 29 | ax.tick_params(axis='x', pad=7) 30 | ax.tick_params(axis='y', pad=8) 31 | if labels is None: 32 | labels = overlaps.columns.tolist() 33 | ax.set_yticklabels(labels, rotation=0) 34 | ax.set_xticks(np.arange(len(labels))+0.75) # offset to align with center 35 | ax.set_xticklabels(labels, rotation=45, ha='right', va='top') 36 | 37 | # Add row and column color annotations 38 | for i, f in enumerate(overlaps): 39 | color = feature_colors[f] 40 | kwargs = dict( 41 | fill=True, facecolor=color, lw=1.5, edgecolor='w', 42 | clip_on=False, zorder=0 43 | ) 44 | p = 0.075 45 | row_color = plt.Rectangle( 46 | (-p, i), p*0.75, 1, transform=ax.get_yaxis_transform(), **kwargs 47 | ) 48 | ax.add_patch(row_color) 49 | p = 0.055 50 | col_color = plt.Rectangle( 51 | (i, -p), 1, p, transform=ax.get_xaxis_transform(), **kwargs 52 | ) 53 | ax.add_patch(col_color) 54 | 55 | return fig 56 | 57 | 58 | def plot_ratios( 59 | ratios: pd.Series, 60 | cmap, 61 | ax, 62 | row_colors: Optional[pd.Series] = None, 63 | ): 64 | # Cap infinity and negative infinity values 65 | mask = ratios.ne(0) & ratios.ne(np.inf) 66 | log_ratios = ratios.copy() 67 | log_ratios.loc[mask] = np.log(ratios.loc[mask]) 68 | offset = log_ratios[mask].abs().max() * 0.05 69 | vmin = log_ratios[mask].min() 70 | vmax = log_ratios[mask].max() 71 | log_ratios.replace({0: vmin - offset, np.inf: vmax + offset}, inplace=True) 72 | 73 | # Setup colors 74 | colors = log_ratios.copy() 75 | colors[colors > 0] += vmax / 10 76 | colors[colors < 0] -= vmin / 10 77 | absmax = colors.abs().max() 78 | colors = (colors + absmax) / (absmax * 2) 79 | colors = colors.apply(cmap) 80 | 81 | # Plot log-ratios 82 | x = np.power(np.e, log_ratios) - 1 83 | n = log_ratios.shape[0] 84 | y = np.arange(n) 85 | left = np.repeat(1, n) 86 | ax.barh(y, x, height=0.75, lw=0, left=left, color=colors) 87 | 88 | # Formatting 89 | ax.spines[['top', 'left', 'right']].set_visible(False) 90 | ax.set_ylim(-1.25, n+0.25) 91 | ax.set_yticks(y, log_ratios.index) 92 | ax.tick_params(axis='y', length=0, labelsize=6, pad=6) 93 | ax.tick_params(axis='x', direction='in', labelsize=6) 94 | ax.set_xscale('log') 95 | 96 | # Add row and column color annotations 97 | for i, color in enumerate(row_colors.loc[log_ratios.index]): 98 | kwargs = dict( 99 | fill=True, facecolor=color, lw=1.5, edgecolor='w', 100 | clip_on=False, zorder=0 101 | ) 102 | row_color = plt.Rectangle( 103 | (-0.08, i-0.5), 104 | 0.06, 1, transform=ax.get_yaxis_transform(), **kwargs 105 | ) 106 | ax.add_patch(row_color) 107 | 108 | 109 | def plot_fractions( 110 | fractions: pd.DataFrame, 111 | cmap: dict, 112 | ax, 113 | row_colors: pd.Series, 114 | ): 115 | # Plot cumulative fractions 116 | lefts = np.zeros(fractions.shape[0]) 117 | yvals = np.arange(fractions.shape[0]) 118 | for col in fractions.columns: 119 | ax.barh( 120 | y=yvals, 121 | width=fractions[col], 122 | height=0.75, 123 | left=lefts, 124 | color=cmap[col], 125 | lw=0., 126 | edgecolor='w', 127 | ) 128 | lefts += fractions[col] 129 | if 'Osteoblast' in col: 130 | styles = dict(lw=0.5, color='k', clip_on=False) 131 | for x, y in zip(lefts, yvals): 132 | ax.plot([x]*2, [y-0.5, y+0.5], **styles) 133 | 134 | # Formatting 135 | ax.spines[['top', 'left', 'right']].set_visible(False) 136 | ax.set_ylim(-1.25, fractions.shape[0] + 0.25) 137 | ax.set_yticks(yvals, fractions.index) 138 | ax.tick_params(axis='y', length=0, labelsize=6, pad=6) 139 | ax.tick_params(axis='x', direction='in', labelsize=6) 140 | 141 | # Add row and column color annotations 142 | for i, color in enumerate(row_colors.loc[fractions.index]): 143 | kwargs = dict( 144 | fill=True, facecolor=color, lw=1.5, edgecolor='w', 145 | clip_on=False, zorder=0 146 | ) 147 | row_color = plt.Rectangle( 148 | (-0.08, i-0.5), 149 | 0.06, 1, transform=ax.get_yaxis_transform(), **kwargs 150 | ) 151 | ax.add_patch(row_color) -------------------------------------------------------------------------------- /src/utils/pp/assets/ribosomal_genes.gmt: -------------------------------------------------------------------------------- 1 | RIBOSOMAL_GENES_ALL GM10020 GM10031 GM10036 GM10053 GM10073 GM10076 GM10093 GM10101 GM10110 GM10126 GM10131 GM10146 GM10184 GM10250 GM10260 GM10263 GM10269 GM10273 GM10282 GM10320 GM10382 GM10392 GM10451 GM10505 GM10643 GM10762 GM10767 GM10827 GM10837 GM10874 GM10941 GM11110 GM11205 GM11214 GM1123 GM11273 GM11579 GM11707 GM11808 GM12117 GM12166 GM12185 GM12216 GM12253 GM12258 GM12355 GM12728 GM12840 GM13212 GM13830 GM13842 GM14214 GM14288 GM14295 GM14296 GM14305 GM14322 GM14325 GM14326 GM14391 GM14403 GM14410 GM14418 GM14419 GM14698 GM15013 GM15232 GM15234 GM15326 GM15440 GM15446 GM15473 GM156 GM15800 GM15834 GM15892 GM16181 GM16225 GM16253 GM16286 GM16386 GM16519 GM16576 GM16602 GM16702 GM1673 GM16861 GM16867 GM16973 GM17018 GM17087 GM17259 GM17275 GM17322 GM17334 GM17354 GM17399 GM17430 GM17484 GM17509 GM17518 GM17541 GM17552 GM17586 GM17655 GM17669 GM17767 GM17949 GM18025 GM19325 GM19585 GM19684 GM19705 GM1976 GM2000 GM2004 GM20045 GM20109 GM20186 GM2026 GM20342 GM20517 GM20604 GM20696 GM20707 GM20939 GM21269 GM21887 GM21967 GM21975 GM21992 GM21994 GM2237 GM26510 GM26514 GM26517 GM26518 GM26520 GM26522 GM26525 GM26526 GM26531 GM26532 GM26533 GM26534 GM26541 GM26542 GM26545 GM26549 GM26551 GM26563 GM26590 GM26609 GM26610 GM26614 GM26615 GM26619 GM26637 GM26640 GM26656 GM26664 GM26669 GM26690 GM26692 GM26698 GM26699 GM26720 GM26724 GM26733 GM26734 GM26762 GM26767 GM26782 GM26785 GM26789 GM26799 GM26802 GM26809 GM26825 GM26830 GM26847 GM26853 GM26867 GM26877 GM26882 GM26884 GM26888 GM26890 GM26901 GM26909 GM26910 GM26916 GM26917 GM26981 GM27010 GM27017 GM27029 GM27162 GM2800 GM28041 GM28053 GM28068 GM28187 GM28285 GM28347 GM28557 GM28707 GM28873 GM28874 GM28875 GM28935 GM29243 GM29336 GM29394 GM29562 GM29666 GM2A GM340 GM34086 GM3468 GM35315 GM3550 GM3604 GM3636 GM36445 GM37170 GM37233 GM37387 GM37494 GM3839 GM4070 GM42031 GM4208 GM42372 GM42418 GM42595 GM42715 GM42743 GM42903 GM42921 GM42997 GM43042 GM43291 GM43464 GM4349 GM43597 GM43672 GM43698 GM43703 GM43796 GM43848 GM44066 GM44067 GM44148 GM44174 GM44175 GM44238 GM44728 GM44751 GM45028 GM45035 GM45036 GM45069 GM45123 GM45184 GM45351 GM45353 GM45509 GM45620 GM4631 GM4707 GM4724 GM4799 GM4924 GM4950 GM4951 GM5093 GM5111 GM5113 GM5127 GM5141 GM5145 GM5148 GM5160 GM5218 GM5239 GM5424 GM5426 GM5449 GM5547 GM5580 GM561 GM5617 GM5786 GM5914 GM6133 GM614 GM6169 GM6225 GM6297 GM6525 GM6563 GM6576 GM6710 GM6712 GM6768 GM6793 GM6904 GM7102 GM7160 GM7334 GM7535 GM7879 GM7967 GM8013 GM8186 GM8225 GM8369 GM8444 GM8773 GM8797 GM8817 GM8953 GM8973 GM8994 GM9242 GM9493 GM9774 GM9776 GM9797 GM9803 GM9843 GM9844 GM9949 GM9958 MRPL1 MRPL10 MRPL11 MRPL12 MRPL13 MRPL14 MRPL15 MRPL16 MRPL17 MRPL18 MRPL19 MRPL2 MRPL20 MRPL21 MRPL22 MRPL23 MRPL24 MRPL27 MRPL28 MRPL3 MRPL30 MRPL32 MRPL33 MRPL34 MRPL35 MRPL36 MRPL37 MRPL38 MRPL39 MRPL4 MRPL40 MRPL41 MRPL42 MRPL43 MRPL44 MRPL45 MRPL46 MRPL47 MRPL48 MRPL49 MRPL50 MRPL51 MRPL52 MRPL53 MRPL54 MRPL55 MRPL57 MRPL58 MRPL9 MRPS10 MRPS11 MRPS12 MRPS14 MRPS15 MRPS16 MRPS17 MRPS18A MRPS18B MRPS18C MRPS2 MRPS21 MRPS22 MRPS23 MRPS24 MRPS25 MRPS26 MRPS27 MRPS28 MRPS30 MRPS31 MRPS33 MRPS34 MRPS35 MRPS36 MRPS5 MRPS6 MRPS7 MRPS9 PRPS1 PRPS1L3 PRPS2 PRPSAP1 PRPSAP2 RPL10 RPL10-PS3 RPL10A RPL11 RPL12 RPL13 RPL13-PS3 RPL13A RPL13A-PS1 RPL14 RPL15 RPL17 RPL18 RPL18A RPL19 RPL21 RPL21-PS4 RPL22 RPL22L1 RPL23 RPL23A RPL23A-PS3 RPL24 RPL26 RPL27 RPL27-PS3 RPL27A RPL28 RPL29 RPL3 RPL30 RPL31 RPL32 RPL34 RPL35 RPL35A RPL36 RPL36-PS3 RPL36A RPL36AL RPL37 RPL37A RPL38 RPL39 RPL39L RPL4 RPL41 RPL5 RPL6 RPL6L RPL7 RPL7A RPL7A-PS3 RPL7A-PS5 RPL7L1 RPL8 RPL9 RPL9-PS1 RPL9-PS6 RPLP0 RPLP1 RPLP2 RPS10 RPS11 RPS12 RPS12-PS3 RPS13 RPS14 RPS15 RPS15A RPS16 RPS17 RPS18 RPS19 RPS19BP1 RPS2 RPS2-PS6 RPS20 RPS21 RPS23 RPS24 RPS25 RPS26 RPS27 RPS27A RPS27L RPS27RT RPS28 RPS29 RPS3 RPS3A1 RPS4X RPS5 RPS6 FAU RPL10L RPL26L1 RPL3L RPS3A RPS4Y1 RPS7 RPS8 RPS9 RPSA RSL24D1 RSL24D1P11 UBA52 RPS4Y2 RPS10P5 RPL39P5 RPLP0P6 GM10062 GM10306 GM1043 GM10521 GM10638 GM10642 GM10655 GM10840 GM11261 GM11454 GM11464 GM11674 GM12057 GM12088 GM12107 GM12184 GM12474 GM13166 GM13218 GM14085 GM14327 GM15340 GM15675 GM15706 GM15853 GM16084 GM16196 GM17305 GM17324 GM17349 GM17455 GM1818 GM19331 GM20219 GM20498 GM20506 GM20721 GM20878 GM2093 GM21781 GM26511 GM26521 GM26566 GM26583 GM26601 GM26620 GM26732 GM26764 GM26766 GM26798 GM26808 GM26835 GM26860 GM26870 GM26885 GM26887 GM26964 GM27019 GM27042 GM28048 GM28556 GM29609 GM29642 GM29650 GM31108 GM31363 GM33994 GM35000 GM3512 GM35339 GM3696 GM3716 GM37276 GM37294 GM3739 GM37933 GM3854 GM42428 GM42555 GM43062 GM43254 GM43332 GM43661 GM43740 GM4419 GM45055 GM45155 GM45250 GM45423 GM45599 GM4787 GM5134 GM527 GM5432 GM5533 GM5608 GM568 GM6213 GM7008 GM8251 GM9008 GM9484 GM9833 GM9889 GM9903 GM9923 RP23-128C4.4 RP23-162P10.8 RP23-181A8.2 RP23-181A8.4 RP23-181A8.7 RP23-186O3.13 RP23-220F20.2 RP23-240G3.3 RP23-242K3.5 RP23-265B15.1 RP23-292G1.2 RP23-312B17.2 RP23-353K11.3 RP23-381H23.1 RP23-395M5.6 RP23-396C4.2 RP23-440L7.5 RP23-72D18.1 RP24-122D14.3 RP24-175C20.18 RP24-282C4.3 RP24-286J21.7 RP24-318O6.3 RP24-496O17.7 2 | RIBOSOMAL_GENES_HUMAN MRPL1 MRPL10 MRPL11 MRPL12 MRPL13 MRPL14 MRPL15 MRPL16 MRPL17 MRPL18 MRPL19 MRPL2 MRPL20 MRPL21 MRPL22 MRPL23 MRPL24 MRPL27 MRPL28 MRPL3 MRPL30 MRPL32 MRPL33 MRPL34 MRPL35 MRPL36 MRPL37 MRPL38 MRPL39 MRPL4 MRPL40 MRPL41 MRPL42 MRPL43 MRPL44 MRPL45 MRPL46 MRPL47 MRPL48 MRPL49 MRPL50 MRPL51 MRPL52 MRPL53 MRPL54 MRPL55 MRPL57 MRPL58 MRPL9 MRPS10 MRPS11 MRPS12 MRPS14 MRPS15 MRPS16 MRPS17 MRPS18A MRPS18B MRPS18C MRPS2 MRPS21 MRPS22 MRPS23 MRPS24 MRPS25 MRPS26 MRPS27 MRPS28 MRPS30 MRPS31 MRPS33 MRPS34 MRPS35 MRPS36 MRPS5 MRPS6 MRPS7 MRPS9 PRPS1 PRPS1L3 PRPS2 PRPSAP1 PRPSAP2 RPL10 RPL10-PS3 RPL10A RPL11 RPL12 RPL13 RPL13-PS3 RPL13A RPL13A-PS1 RPL14 RPL15 RPL17 RPL18 RPL18A RPL19 RPL21 RPL21-PS4 RPL22 RPL22L1 RPL23 RPL23A RPL23A-PS3 RPL24 RPL26 RPL27 RPL27-PS3 RPL27A RPL28 RPL29 RPL3 RPL30 RPL31 RPL32 RPL34 RPL35 RPL35A RPL36 RPL36-PS3 RPL36A RPL36AL RPL37 RPL37A RPL38 RPL39 RPL39L RPL4 RPL41 RPL5 RPL6 RPL6L RPL7 RPL7A RPL7A-PS3 RPL7A-PS5 RPL7L1 RPL8 RPL9 RPL9-PS1 RPL9-PS6 RPLP0 RPLP1 RPLP2 RPS10 RPS11 RPS12 RPS12-PS3 RPS13 RPS14 RPS15 RPS15A RPS16 RPS17 RPS18 RPS19 RPS19BP1 RPS2 RPS2-PS6 RPS20 RPS21 RPS23 RPS24 RPS25 RPS26 RPS27 RPS27A RPS27L RPS27RT RPS28 RPS29 RPS3 RPS3A1 RPS4X RPS5 RPS6 FAU RPL10L RPL26L1 RPL3L RPS3A RPS4Y1 RPS7 RPS8 RPS9 RPSA RSL24D1 RSL24D1P11 UBA52 RPS4Y2 RPS10P5 RPL39P5 RPLP0P6 RP23-128C4.4 RP23-162P10.8 RP23-181A8.2 RP23-181A8.4 RP23-181A8.7 RP23-186O3.13 RP23-220F20.2 RP23-240G3.3 RP23-242K3.5 RP23-265B15.1 RP23-292G1.2 RP23-312B17.2 RP23-353K11.3 RP23-381H23.1 RP23-395M5.6 RP23-396C4.2 RP23-440L7.5 RP23-72D18.1 RP24-122D14.3 RP24-175C20.18 RP24-282C4.3 RP24-286J21.7 RP24-318O6.3 RP24-496O17.7 -------------------------------------------------------------------------------- /src/utils/pp/preprocess.py: -------------------------------------------------------------------------------- 1 | import scanpy as sc 2 | import numpy as np 3 | from typing import List 4 | import sys 5 | import os 6 | import pandas as pd 7 | import warnings 8 | from tqdm.autonotebook import tqdm 9 | import logging 10 | 11 | 12 | # Warnings to ignore throughout 13 | sc.settings.verbosity = 0 14 | warnings.simplefilter("ignore", UserWarning) 15 | logging.raiseExceptions = False 16 | 17 | 18 | # Class to suppress output from inside functions (e.g., PhenoGraph) 19 | class HiddenPrints: 20 | def __enter__(self): 21 | self._original_stdout = sys.stdout 22 | sys.stdout = open(os.devnull, "w") 23 | 24 | def __exit__(self, exc_type, exc_val, exc_tb): 25 | sys.stdout.close() 26 | sys.stdout = self._original_stdout 27 | 28 | 29 | def preprocess( 30 | adata: sc.AnnData, 31 | show_progress: bool = True, 32 | **kwargs, 33 | ): 34 | # Set all arguments below with defaults 35 | defaults = { 36 | 'hvgs': None, 37 | 'hvgs__n': 2000, 38 | 'hvgs__whitelist': None, 39 | 'pca__var_explained': 0.67, 40 | 'pca__max_comps': 1000, 41 | 'cluster': True, 42 | 'cluster__k': 30, 43 | 'neighbors': True, 44 | 'neighbors__k': 30, 45 | 'umap': True, 46 | 'umap__random_state': 1, 47 | 'impute': True, 48 | 'impute__k': 5, 49 | 'impute__t': 3, 50 | } 51 | pp_args = defaults 52 | pp_args.update(kwargs) 53 | 54 | n_steps = 4 55 | for key in ['cluster', 'neighbors', 'umap', 'impute']: 56 | if bool(pp_args[key]): n_steps += 1 57 | if show_progress: pbar = tqdm(total=n_steps) 58 | 59 | # Median library-size normalization 60 | if show_progress: pbar.set_description('Normalizing') 61 | adata.layers['median'] = adata.layers['raw'].copy() 62 | sc.pp.normalize_total(adata, layer='median') 63 | if show_progress: pbar.update(1) 64 | 65 | # Log-transformation (natural log, pseudocount of 1) 66 | if show_progress: pbar.set_description('Log-transforming') 67 | adata.layers['log'] = adata.layers['median'].copy() 68 | sc.pp.log1p(adata, layer='log') 69 | if show_progress: pbar.update(1) 70 | 71 | # Set HVGs 72 | if show_progress: pbar.set_description('Setting HVGs') 73 | hvgs = pp_args['hvgs'] 74 | if hvgs is None: 75 | hvgs = get_hvgs(adata, pp_args['hvgs__n'], pp_args['hvgs__whitelist']) 76 | adata.var['highly_variable'] = adata.var.index.isin(hvgs) 77 | n_hvgs = adata.var['highly_variable'].sum() 78 | if show_progress: pbar.update(1) 79 | 80 | # PCA 81 | if show_progress: pbar.set_description('Running PCA') 82 | adata.X = adata.layers['log'] 83 | n_comps = min(pp_args['pca__max_comps'], *adata.shape, n_hvgs) - 2 84 | sc.tl.pca(adata, n_comps=n_comps, use_highly_variable=True) 85 | X_pca_full = adata.obsm['X_pca'].copy() 86 | cum_vars = adata.uns['pca']['variance_ratio'].cumsum() 87 | n_comps = np.argmin(abs(cum_vars - pp_args['pca__var_explained'])) 88 | adata.obsm['X_pca'] = X_pca_full[:, :n_comps] 89 | if show_progress: pbar.update(1) 90 | 91 | # Cluster with PhenoGraph 92 | if pp_args['cluster']: 93 | if show_progress: pbar.set_description('Clustering with PhenoGraph') 94 | with HiddenPrints(): 95 | communities, _, _ = sc.external.tl.phenograph( 96 | pd.DataFrame(adata.obsm['X_pca']), 97 | k=pp_args['cluster__k'], 98 | nn_method='brute', 99 | njobs=-1, 100 | ) 101 | adata.obs['PhenoGraph_clusters'] = pd.Categorical(communities) 102 | if show_progress: pbar.update(1) 103 | 104 | # Nearest neighbors in PC space 105 | if pp_args['neighbors']: 106 | if show_progress: pbar.set_description('Finding nearest neighbors') 107 | sc.pp.neighbors( 108 | adata, 109 | use_rep='X_pca', 110 | n_neighbors=pp_args['neighbors__k'] 111 | ) 112 | if show_progress: pbar.update(1) 113 | 114 | # UMAP 115 | if pp_args['umap']: 116 | if show_progress: pbar.set_description('Calculating UMAP') 117 | # Default to PAGA with clusters if clustering, else spectral 118 | init_pos = 'spectral' 119 | if pp_args['cluster']: 120 | sc.tl.paga(adata, groups='PhenoGraph_clusters') 121 | sc.pl.paga(adata, plot=False) 122 | init_pos="paga" 123 | sc.tl.umap(adata, random_state=1, init_pos=init_pos) 124 | if show_progress: pbar.update(1) 125 | 126 | # Impute expression with MAGIC 127 | if pp_args['impute']: 128 | if show_progress: pbar.set_description('Imputing expression with MAGIC') 129 | adata.X = adata.layers["log"] 130 | with HiddenPrints(): 131 | try: 132 | adata_magic = sc.external.pp.magic( 133 | adata, 134 | copy=True, 135 | n_pca=n_comps, 136 | knn=pp_args['impute__k'], 137 | t=pp_args['impute__t'], 138 | verbose=False, 139 | ) 140 | except ValueError as e: 141 | pass 142 | adata.layers['imputed'] = adata_magic.X 143 | if show_progress: pbar.update(1) 144 | 145 | return adata 146 | 147 | 148 | def get_hvgs( 149 | adata: sc.AnnData, 150 | n_hvgs: int, 151 | whitelist: List[str], 152 | ): 153 | hvgs = sc.pp.highly_variable_genes( 154 | adata, 155 | layer='raw', 156 | n_top_genes=n_hvgs, 157 | n_bins=1000, 158 | flavor='seurat_v3', 159 | inplace=False, 160 | ) 161 | hvgs['rank'] = hvgs['highly_variable_rank'] 162 | 163 | # Remove genes in blacklist from HVGs 164 | cwd = os.path.dirname(os.path.realpath(__file__)) 165 | ribosomal = pd.read_csv( 166 | f'{cwd}/assets/ribosomal_genes.gmt', 167 | sep='\t', 168 | index_col=0, 169 | header=None 170 | ).loc['RIBOSOMAL_GENES_HUMAN'].iloc[1:].dropna().tolist() 171 | mitochondrial = hvgs.index[hvgs.index.str.startswith('MT-')].tolist() 172 | blacklist = set(ribosomal).union(mitochondrial) 173 | blacklist = hvgs.index.intersection(blacklist) 174 | for gene in blacklist: 175 | rank = hvgs.loc[gene, 'rank'] 176 | if not np.isnan(rank): 177 | hvgs.loc[gene, 'rank'] = np.nan # remove this gene from rank 178 | below_rank = hvgs['rank'].gt(rank) & ~hvgs['rank'].isna() 179 | hvgs.loc[below_rank, 'rank'] -= 1 # move everything else up 1 180 | 181 | # Final list is union of HVGs with whitelist 182 | hvgs = hvgs.dropna().sort_values('rank').iloc[:n_hvgs] 183 | hvgs = hvgs.index.union(whitelist).tolist() 184 | 185 | return hvgs -------------------------------------------------------------------------------- /src/utils/pl/plot_embedding.py: -------------------------------------------------------------------------------- 1 | import scanpy as sc 2 | from collections.abc import Iterable 3 | from typing import Union, List 4 | import itertools 5 | from mpl_toolkits.axes_grid1 import make_axes_locatable 6 | from matplotlib import pyplot as plt 7 | import math 8 | import os 9 | 10 | # Set style sheet on import 11 | cwd = os.path.dirname(os.path.realpath(__file__)) 12 | plt.style.use(f'{cwd}/assets/default.mplstyle') 13 | 14 | def format_ax( 15 | fig, ax, 16 | style="umap", 17 | title="", 18 | cbar=True, 19 | dim_label="UMAP", 20 | fs=12, 21 | lw=1.5, 22 | arrow_len=0.2, 23 | draw_arrows=True, 24 | ): 25 | ax.set_facecolor('white') 26 | ax.set_xticklabels([]) 27 | ax.set_yticklabels([]) 28 | ax.get_xaxis().set_visible(False) 29 | ax.get_yaxis().set_visible(False) 30 | ax.grid(False) 31 | ax.spines[list(ax.spines)].set_visible(False) 32 | 33 | if style == "umap": 34 | change_aspect(ax) 35 | if draw_arrows: 36 | arrowed_spines(ax, arrow_len, text=dim_label, fs=fs, lw=lw) 37 | ax.set_title(title, weight="bold") 38 | if cbar: 39 | format_cbar(fig, ax) 40 | 41 | 42 | def format_cbar(fig, ax): 43 | 44 | cbar = ax.get_children()[0].colorbar 45 | if cbar: 46 | cbar.remove() 47 | data = ax.get_children()[0] 48 | 49 | # Create colorbar ax 50 | bbox = ax.get_position() 51 | cax = fig.add_axes([ 52 | bbox.x1+bbox.width*0.025, #min x 53 | bbox.y0+bbox.height*0.25, #min y 54 | bbox.width*0.03, #width 55 | bbox.height*0.5 #height 56 | ]) 57 | cax.grid(False) 58 | new_cbar = fig.colorbar( 59 | data, ax=ax, cax=cax, 60 | ) 61 | new_cbar.outline.set_visible(False) 62 | if not cbar: 63 | bbox = ax.get_position() 64 | ax.get_children()[0].colorbar.remove() 65 | ax.set_position(bbox) 66 | 67 | 68 | def change_aspect(ax): 69 | 70 | # Reset x and y limits for square plotting 71 | xmin, xmax = ax.get_xlim() 72 | xrange = xmax - xmin 73 | xcenter = (xrange/2) + xmin 74 | 75 | ymin, ymax = ax.get_ylim() 76 | yrange = ymax - ymin 77 | ycenter = (yrange/2) + ymin 78 | 79 | axrange = max(xrange, yrange)/2 80 | 81 | xmin = xcenter - (axrange) 82 | xmax = xcenter + (axrange) 83 | ax.set_xlim(xmin, xmax) 84 | 85 | ymin = ycenter - (axrange) 86 | ymax = ycenter + (axrange) 87 | ax.set_ylim(ymin, ymax) 88 | 89 | ax.set_aspect('equal', adjustable = 'box') 90 | 91 | 92 | def arrowed_spines( 93 | ax, 94 | length = 0.2, 95 | text = None, 96 | fs = None, 97 | lw = 1.5, 98 | ): 99 | xmin, xmax = ax.get_xlim() 100 | ymin, ymax = ax.get_ylim() 101 | 102 | hw = 1./30.*(ymax-ymin) 103 | hl = 1./30.*(xmax-xmin) 104 | lw = lw # axis line width 105 | ohg = 0.0 # arrow overhang 106 | 107 | ax.spines[list(ax.spines)].set_visible(False) 108 | ax.arrow( 109 | xmin, ymin, (xmax-xmin)*length, 0, fc='k', ec='k', lw = lw, 110 | head_width=hw, head_length=hl, overhang = ohg, 111 | length_includes_head= True, clip_on = False 112 | ) 113 | ax.arrow( 114 | xmin, ymin, 0, (ymax-ymin)*length, fc='k', ec='k', lw = lw, 115 | head_width=hw, head_length=hl, overhang = ohg, 116 | length_includes_head= True, clip_on = False 117 | ) 118 | if fs == None: 119 | fs = plt.rcParams["xtick.labelsize"] 120 | ax.text( 121 | s=f"{text}1", 122 | y=ymin-(ymax-ymin)*0.05, x=xmin+(xmax-xmin)*length/2, 123 | ha="center", va="top", 124 | fontsize = fs 125 | ) 126 | ax.text( 127 | s=f"{text}2", 128 | x=xmin-(xmax-xmin)*0.05, y=ymin+(ymax-ymin)*length/2, 129 | ha="right", va="center", rotation=90, 130 | fontsize = fs 131 | ) 132 | ax.set_xlim(xmin, xmax) 133 | ax.set_ylim(ymin, ymax) 134 | 135 | 136 | def plot_embedding( 137 | adata: sc.AnnData, 138 | features: Union[str, List[str]], 139 | basis: str = 'X_umap', 140 | palette: str = "tab20", 141 | cmap: str = "plasma", 142 | titles: Union[str, List[str]] = None, 143 | ncols: int = 5, 144 | dim: int = 5, 145 | layer: str = "imputed", 146 | dim_label = "UMAP", 147 | ax = None, 148 | fs: int = 12, 149 | lw: float = 1.5, 150 | arrow_len: float = 0.2, 151 | draw_arrows=False, 152 | rasterized=False, 153 | **kwargs, 154 | ): 155 | iterify = lambda x: x if isinstance(x, Iterable) and not isinstance(x, str) else [x] 156 | features = iterify(features) 157 | titles = iterify(titles) 158 | if not ax: 159 | nrows = math.ceil(len(features)) 160 | fig, axes = plt.subplots( 161 | nrows, ncols, 162 | figsize=(dim*ncols,dim*nrows), 163 | ) 164 | fig.tight_layout(pad=dim*0.75) 165 | axes = axes.flat if isinstance(axes, Iterable) else [axes] 166 | else: 167 | assert (len(features)==1) and (len(titles)==1) 168 | fig = ax.get_figure() 169 | axes = [ax] 170 | for ax, feature, title in itertools.zip_longest(axes, features, titles): 171 | if not title: title = feature 172 | if feature: 173 | sc.pl.embedding( 174 | adata, 175 | basis=basis, 176 | color=feature, 177 | ax=ax, 178 | show=False, 179 | palette=palette, cmap=cmap, 180 | layer=layer, 181 | **kwargs, 182 | ) 183 | if rasterized: 184 | ax.get_children()[0].set_rasterized(True) 185 | format_ax( 186 | fig, ax, style="umap", 187 | title=title, dim_label=dim_label, fs=fs, 188 | arrow_len=arrow_len, lw=lw, draw_arrows=draw_arrows, 189 | ) 190 | else: 191 | ax.set_visible(False) 192 | return fig 193 | 194 | 195 | def lighten_color(color, amount=0.5): 196 | import matplotlib.colors as mc 197 | import colorsys 198 | try: 199 | c = mc.cnames[color] 200 | except: 201 | c = color 202 | c = colorsys.rgb_to_hls(*mc.to_rgb(c)) 203 | return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) 204 | 205 | 206 | def saturate(c, s=1.0): 207 | from matplotlib import colors 208 | from collections.abc import Iterable 209 | # Assumed to be hex if string 210 | if isinstance(c, str): 211 | rgb = colors.to_rgb(c) 212 | hex = True 213 | # Assumed to be RGB if iterable 214 | elif isinstance(c, Iterable): 215 | rgb = c 216 | hex = False 217 | hsv = colors.rgb_to_hsv(rgb) 218 | hsv[1] *= s 219 | c = colors.hsv_to_rgb(hsv) 220 | if hex: 221 | return colors.to_hex(c) 222 | else: 223 | return c -------------------------------------------------------------------------------- /src/utils/tl/transfer_labels.py: -------------------------------------------------------------------------------- 1 | import scipy as sp 2 | import numpy as np 3 | import scanpy as sc 4 | import pandas as pd 5 | from sklearn.neighbors import NearestNeighbors 6 | from phenograph.classify import random_walk_probabilities 7 | from sklearn.linear_model import LinearRegression 8 | 9 | 10 | # Deal with font/plotting bugs introduced by Harmony 11 | from matplotlib import pyplot as plt 12 | import seaborn as sns 13 | font = plt.rcParams['font.family'] 14 | backend = plt.rcParams['backend'] 15 | import harmony.core 16 | sns.set(font=font) 17 | plt.rcParams['backend'] = backend 18 | 19 | 20 | # Get affinity matrix for subset of AnnData (not augmented) 21 | def get_affinity_matrix( 22 | adata: sc.AnnData, 23 | n_neighbors: int, 24 | metric: str, 25 | ): 26 | # Get nearest neighbors 27 | sc.pp.neighbors( 28 | adata, 29 | use_rep='X_pca', 30 | n_neighbors=n_neighbors, 31 | metric=metric, 32 | ) 33 | dists = adata.obsp['distances'] 34 | 35 | # Get the (adaptive) kth nearest neighbor for each cell 36 | adaptive_k = int(np.floor(n_neighbors/3)) 37 | adaptive_std = np.zeros(adata.shape[0]) 38 | for i, chunk in enumerate(np.split(dists.data, dists.indptr[1:-1])): 39 | adaptive_std[i] = np.sort(chunk)[adaptive_k-1] 40 | 41 | # Normalize dists and return diffusion kernel 42 | i, j, d = sp.sparse.find(dists) 43 | d /= adaptive_std[i] # normalize dists by the (adaptive) kth nearest neighbor 44 | W = sp.sparse.csr_matrix( 45 | (np.exp(-d), (i,j)), 46 | shape=adata.obsp['distances'].shape, 47 | ) 48 | kernel = W + W.T 49 | 50 | return kernel, pd.Series(adaptive_std, index=adata.obs.index) 51 | 52 | 53 | # Function to construct mutually nearest neighbors bewteen two datasets 54 | def construct_mnn( 55 | t1_cells, 56 | t2_cells, 57 | data_df: pd.DataFrame, 58 | n_neighbors: int, 59 | metric: str, 60 | n_jobs=-2, 61 | ): 62 | nbrs = NearestNeighbors( 63 | n_neighbors=n_neighbors, 64 | metric=metric, 65 | n_jobs=n_jobs 66 | ) 67 | t1_data = data_df.loc[t1_cells, :].values 68 | t2_data = data_df.loc[t2_cells, :].values 69 | # Dataset 1 neighbors 70 | nbrs.fit(t1_data) 71 | t1_nbrs = nbrs.kneighbors_graph(t2_data, mode='distance') 72 | 73 | # Dataset 2 neighbors 74 | nbrs.fit(t2_data) 75 | t2_nbrs = nbrs.kneighbors_graph(t1_data, mode='distance') 76 | 77 | # Mututally nearest neighbors 78 | mnn = t2_nbrs.multiply(t1_nbrs.T) 79 | mnn = mnn.sqrt() 80 | return mnn 81 | 82 | 83 | # From Harmony 84 | def mnn_ka_distances(mnn, n_neighbors): 85 | # Function to find distance kth neighbor in the mutual nearest neighbor matrix 86 | ka = int(n_neighbors / 3) 87 | ka_dists = np.repeat(None, mnn.shape[0]) 88 | x, y, z = sp.sparse.find(mnn) 89 | rows=pd.Series(x).value_counts() 90 | for r in rows.index[rows >= ka]: 91 | ka_dists[r] = np.sort(z[x==r])[ka - 1] 92 | return ka_dists 93 | 94 | 95 | # From Harmony 96 | def mnn_scaling_factors(mnn_ka_dists, scaling_factors): 97 | cells = mnn_ka_dists.index[~mnn_ka_dists.isnull()] 98 | # Linear model fit 99 | x = scaling_factors[cells] 100 | y = mnn_ka_dists[cells] 101 | lm = LinearRegression() 102 | lm.fit(x.values.reshape(-1, 1), y.values.reshape(-1, 1)) 103 | # Predict 104 | x = scaling_factors[mnn_ka_dists.index] 105 | vals = np.ravel(lm.predict(x.values.reshape(-1, 1))) 106 | mnn_scaling_factors = pd.Series(vals, index=mnn_ka_dists.index) 107 | return mnn_scaling_factors 108 | 109 | 110 | def get_mnn_affinity_function( 111 | index_1, 112 | index_2, 113 | scaling_factors, 114 | adata, 115 | n_neighbors: int, 116 | metric: str, 117 | ): 118 | sorted_index = index_1.append(index_2) 119 | 120 | # Construct MNN between groups 121 | mnn = construct_mnn( 122 | index_1, index_2, 123 | pd.DataFrame(adata.obsm['X_pca'], index=adata.obs.index), 124 | n_neighbors, 125 | metric=metric 126 | ) 127 | # MNN adaptive distances 128 | ka_dists = pd.Series(0.0, index=sorted_index) # distance to (adaptive) kth neighbor 129 | ka_dists[index_1] = mnn_ka_distances(mnn, n_neighbors) 130 | ka_dists[index_2] = mnn_ka_distances(mnn.T, n_neighbors) 131 | 132 | # MNN scaling factors 133 | mnn_sf = pd.Series(0.0, index=sorted_index) 134 | mnn_sf[index_1] = mnn_scaling_factors( 135 | ka_dists[index_1], scaling_factors, 136 | ) 137 | mnn_sf[index_2] = mnn_scaling_factors( 138 | ka_dists[index_2], scaling_factors, 139 | ) 140 | # MNN affinity matrix 141 | mnn_aff = harmony.core._mnn_affinity( 142 | mnn, mnn_sf, 143 | np.where(adata.obs.index.isin(index_1))[0][0], 144 | np.where(adata.obs.index.isin(index_2))[0][0], 145 | device='cpu', 146 | ) 147 | return mnn_aff 148 | 149 | 150 | # Calculate augmented affinity matrix between labeled and unlabeled datasets 151 | def get_augmented_affinity_matrix( 152 | adata: sc.AnnData, 153 | label_column: str, 154 | knn: int = 30, 155 | mnn: int = 60, 156 | ): 157 | # Assume unlabeled samples have NaN in label column 158 | is_labeled = ~adata.obs[label_column].isna() 159 | 160 | # Affinity matrix for unlabeled data 161 | aff_unl, sf_unl = get_affinity_matrix( 162 | adata[~is_labeled], 163 | n_neighbors=knn, 164 | metric='euclidean', 165 | ) 166 | # Affinity matrix for labeled data 167 | aff_lbl, sf_lbl = get_affinity_matrix( 168 | adata[is_labeled], 169 | n_neighbors=knn, 170 | metric='euclidean', 171 | ) 172 | 173 | # Affinity matrix between labeled and unlabeled data 174 | sf_cmb = pd.concat([sf_unl, sf_lbl]) 175 | adata = adata[sf_cmb.index].copy() # reorder AnnData 176 | mnn_aff = get_mnn_affinity_function( 177 | sf_unl.index, 178 | sf_lbl.index, 179 | sf_cmb, 180 | adata, 181 | n_neighbors=mnn, 182 | metric='cosine', 183 | ) 184 | 185 | # Combine affinity matrices 186 | # Expand shape of affinity matrices to shape of combined AnnData 187 | shape = [adata.obs.shape[0]]*2 # cells x cells 188 | 189 | # In-vitro affinity matrix 190 | i, j, v = sp.sparse.find(aff_unl) 191 | aff_unl_full = sp.sparse.csr_matrix((v, (i, j)), shape=shape) 192 | 193 | # In-vivo affinity matrix 194 | i, j, v = sp.sparse.find(aff_lbl) 195 | offset = aff_unl.shape[0] 196 | aff_lbl_full = sp.sparse.csr_matrix((v, (i+offset, j+offset)), shape=shape) 197 | 198 | # Combine all affinity matrices into symmetric matrix 199 | comb_aff = aff_unl_full + aff_lbl_full + mnn_aff + mnn_aff.T 200 | 201 | return comb_aff 202 | 203 | 204 | def transfer_labels( 205 | adata, 206 | label_column: str, 207 | knn: int = 30, 208 | mnn: int = 60, 209 | ): 210 | # Assume unlabeled samples have NaN in label column 211 | is_labeled = ~adata.obs[label_column].isna() 212 | 213 | # Get labels for known and unknown data 214 | labels = adata.obs.loc[is_labeled, label_column].values 215 | lbl_codes, lbl_uniques = pd.factorize(labels) 216 | lbl_codes = np.append( 217 | np.zeros((np.sum(~is_labeled),), dtype=int), # unlabeled is zero 218 | lbl_codes + 1 219 | ) 220 | 221 | # Map labels using PhenoGraph classify and affinity matrix 222 | A = get_augmented_affinity_matrix(adata, label_column, knn, mnn) 223 | P = random_walk_probabilities(A, lbl_codes) 224 | c = np.argmax(P, axis=1) 225 | 226 | # Annotate AnnData 227 | c_map = dict(enumerate(lbl_uniques)) 228 | adata.obs.loc[~is_labeled, label_column] = pd.Series(c).map(c_map).values 229 | for key, val in c_map.items(): 230 | adata.obs[f'P({val})'] = 0. 231 | adata.obs.loc[is_labeled, f'P({val})'] = 1. 232 | adata.obs.loc[~is_labeled, f'P({val})'] = P.T[key] 233 | -------------------------------------------------------------------------------- /src/utils/pl/plot_trends.py: -------------------------------------------------------------------------------- 1 | from pygam import LinearGAM, s as spline_term 2 | import numpy as np 3 | import pandas as pd 4 | import scanpy as sc 5 | from matplotlib import pyplot as plt 6 | import matplotlib.gridspec as gridspec 7 | from matplotlib.colors import LinearSegmentedColormap as lsc 8 | from typing import List, Tuple, Union, Optional 9 | import scipy as sp 10 | 11 | 12 | def get_gam_trend( 13 | x: np.ndarray, 14 | y: np.ndarray, 15 | n_splines: int = 8, #8 16 | spline_order: int = 4, #3 17 | x_res: int = 500, 18 | weights: Optional[np.ndarray] = None, 19 | ): 20 | x_pred = np.linspace(x.min(), x.max(), x_res) 21 | spline = spline_term(0, n_splines=n_splines, spline_order=spline_order) 22 | gam = LinearGAM(spline).fit(x, y, weights) 23 | y_pred = gam.predict(x_pred) 24 | p = gam.predict(x) 25 | n = len(x) 26 | mu = np.mean(x) 27 | sigma = np.sqrt(((y - p) ** 2).sum() / (n - 2)) 28 | std = ( 29 | np.sqrt( 30 | 1 + 1 / n + (x_pred - mu) ** 2 / ((x - mu) ** 2).sum() 31 | ) * sigma / 2 32 | ) 33 | return pd.Series(y_pred, index=x_pred), pd.Series(std, index=x_pred) 34 | 35 | 36 | def plot_palantir_trends( 37 | adata: sc.AnnData, 38 | features: List[str], 39 | ps_column: str, 40 | branch_column: str, 41 | fig: plt.Figure, 42 | feature_colors: Optional[dict] = None, 43 | gs: Optional[gridspec.GridSpec] = None, 44 | **kwargs, 45 | ): 46 | # Colors for each trend/row 47 | if feature_colors is None: 48 | feature_colors = dict() 49 | cycler = plt.rcParams['axes.prop_cycle'] 50 | for f, props in zip(features, cycler): 51 | feature_colors[f] = props['color'] 52 | 53 | # Plot organization 54 | if gs is None: 55 | gs = fig.add_gridspec(1, 1) 56 | gs_0 = gs.subgridspec(2, 1, hspace=0.5) 57 | ax_1 = fig.add_subplot(gs_0[0]) 58 | ax_2 = fig.add_subplot(gs_0[1], sharex=ax_1) 59 | 60 | # Calculate and plot feature trends for this branch 61 | ps_max = adata.obs.loc[adata.obs[branch_column] > 0.9, ps_column].max() 62 | mask = adata.obs[ps_column].lt(ps_max) 63 | x = adata.obs.loc[mask, ps_column] 64 | weights = adata.obs.loc[mask, branch_column] 65 | # Strengthen weights to account for instability near w=0 66 | eps = 1e-5 67 | weights = weights.clip(eps, 1-eps) ** 2 68 | for f in features: 69 | if f in adata.obs: 70 | y = adata.obs.loc[mask, f] 71 | else: 72 | y = adata[mask, f].layers['palantir_imputed'].flatten() 73 | # Plot trend 74 | trend, std = get_gam_trend(x, y, weights=weights, **kwargs) 75 | trend = sp.stats.zscore(trend) 76 | color = feature_colors[f] 77 | ax_1.plot(trend, color=color) 78 | # Plot first derivative 79 | diffs = pd.Series(np.diff(trend.values, n=1), trend.index[:-1]) 80 | diffs = sp.stats.zscore(diffs) 81 | ax_2.plot(diffs, color=color) 82 | # Mark maxima 83 | signs = np.sign(np.diff(trend.values, n=2)) 84 | try: 85 | max_idx = np.argwhere(signs[:-1] > signs[1:])[0] 86 | except: 87 | max_idx = np.argmax(diffs) 88 | styles = dict(lw=0.5, linestyle='--', color=color) 89 | for ax in [ax_1, ax_2]: 90 | t = ax.get_xaxis_transform() 91 | ax.vlines(trend.index[max_idx], 0, 1, transform=t, **styles) 92 | 93 | # Formatting 94 | ax_1.xaxis.set_visible(False) 95 | for ax in [ax_1, ax_2]: 96 | ax.tick_params(direction='in', length=2, labelsize=5, pad=2) 97 | 98 | 99 | def plot_pseudotime_trends( 100 | adata: sc.AnnData, 101 | ps_column: str, 102 | feature_columns: Union[str, List[str]], 103 | ax, 104 | feature_colors: Optional[dict] = None, 105 | clip: Tuple[float] = (0.2, 1.0), 106 | labels: Optional[List[str]] = None, 107 | **kwargs, 108 | ): 109 | # Colors for each trend/row 110 | if feature_colors is None: 111 | feature_colors = dict() 112 | cycler = plt.rcParams['axes.prop_cycle'] 113 | for f, props in zip(feature_columns, cycler): 114 | feature_colors[f] = props['color'] 115 | 116 | # Calculate feature trends and reformat them as rows in a heatmap 117 | # 2) Find positions where trends increase (maxima in 2nd derivative) 118 | heatmap_trends = [] 119 | tick_positions = [] 120 | x = adata.obs[ps_column] 121 | for f in feature_columns: 122 | y = adata.obs[f] 123 | trend, _ = get_gam_trend(x, y, **kwargs) 124 | norm = plt.Normalize(*trend.quantile(clip)) 125 | trend_cmap = lsc.from_list("", [[1,1,1], feature_colors[f]]) 126 | row = trend.apply(norm).apply(trend_cmap).values.tolist() 127 | heatmap_trends.append(row) 128 | 129 | # Plotting 130 | ax.imshow(heatmap_trends, aspect="auto", interpolation='none',) 131 | borders = np.arange(-0.5, len(feature_columns)+0.5, 1) 132 | for y_pos, x_pos in enumerate(tick_positions): 133 | ax.plot([x_pos]*2, [y_pos-0.5, y_pos+0.5], c='k') 134 | 135 | # Formatting 136 | ax.hlines(borders, -1, 501, color="w", lw=1, zorder=2, clip_on=False) 137 | ax.set_xticks([0, 500], [0., 1.0]) 138 | ax.set_xlim(-5, 505) 139 | ax.set_ylim(len(feature_columns)-0.5+0.2, -0.5-0.2) 140 | # Hide y-axis 141 | ax.yaxis.set_visible(False) 142 | ax.spines['left'].set_visible(False) 143 | 144 | # Row colors 145 | for i, f in enumerate(feature_columns): 146 | color = feature_colors[f] 147 | kwargs = dict( 148 | fill=True, facecolor=color, lw=1.5, edgecolor='w', 149 | clip_on=False, zorder=0 150 | ) 151 | row_color = plt.Rectangle( 152 | (-0.05, i-0.5), 0.04, 1, 153 | transform=ax.get_yaxis_transform(), **kwargs 154 | ) 155 | ax.add_patch(row_color) 156 | 157 | 158 | def plot_module_progressions( 159 | adata: sc.AnnData, 160 | named_colors: dict, 161 | features: List = None, 162 | peaks: List = None, 163 | figsize: Tuple = (2, 1.2), 164 | ): 165 | # Setup figure 166 | fig, axes = plt.subplots( 167 | 2,1, 168 | figsize=figsize, 169 | gridspec_kw=dict(height_ratios=[1, 0.33], hspace=0.33/(1.33/2)), 170 | ) 171 | 172 | # Standard columns to plot 173 | if features is None: 174 | features = [ 175 | 'Module Absorptive Intestine Score', 176 | 'Module Secretory Intestine Score', 177 | 'Module Intestine Score', 178 | 'Module Tumor ISC-like Score', 179 | 'Module Endoderm Development Score', 180 | f"Module {adata.uns['DC Terminal State']} Score", 181 | 'Fetal, Conserved' 182 | ] 183 | 184 | # Plot DC trends for all modules 185 | ax = axes[0] 186 | ps_column = adata.uns['DC'] 187 | plot_pseudotime_trends(adata, ps_column, features, ax, named_colors) 188 | 189 | # Mark positions in fetal and terminal states where trend crosses 0.8 190 | if peaks is None: peaks = features[-2:] 191 | for feature in peaks: 192 | # Calculate trends 193 | trend, _ = get_gam_trend(adata.obs[ps_column], adata.obs[feature]) 194 | trend -= trend.min() 195 | trend /= trend.max() 196 | # Mark positions 197 | signs = np.sign(trend.values - 0.75) 198 | idx = np.argwhere(signs[:-1] < signs[1:]).flatten() 199 | t = ax.get_xaxis_transform() 200 | styles = dict(lw=0.5, linestyle='--', color=named_colors[feature]) 201 | ax.vlines(idx, 0, 1, transform=t, **styles) 202 | 203 | # Plot sample type positions 204 | ax = axes[1] 205 | x = adata.obs[ps_column] 206 | y = adata.obs['Sample Type'].str.contains('Primary') 207 | c = adata.obs['Sample Type'].map(named_colors).tolist() 208 | styles = dict(s=8, lw=0, alpha=0.25, rasterized=True, clip_on=False) 209 | ax.scatter(x, y, c=c, **styles) 210 | 211 | # Formatting 212 | offset = (x.max() - x.min()) / 100 213 | ax.set_xlim(x.min() - offset, x.max() + offset) 214 | ax.set_xticks([x.min(), x.max()], [0., 1.]) 215 | ax.set_ylim(-0.67, 1.67) 216 | ax.yaxis.set_visible(False) 217 | ax.spines['left'].set_visible(False) 218 | 219 | return fig 220 | -------------------------------------------------------------------------------- /notebooks/Figure_4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 107, 6 | "id": "75109198-3a92-414d-ad34-9fbb62693963", 7 | "metadata": { 8 | "execution": { 9 | "iopub.execute_input": "2024-10-29T00:37:21.735490Z", 10 | "iopub.status.busy": "2024-10-29T00:37:21.735145Z", 11 | "iopub.status.idle": "2024-10-29T00:37:21.764685Z", 12 | "shell.execute_reply": "2024-10-29T00:37:21.764180Z", 13 | "shell.execute_reply.started": "2024-10-29T00:37:21.735468Z" 14 | }, 15 | "tags": [] 16 | }, 17 | "outputs": [ 18 | { 19 | "name": "stdout", 20 | "output_type": "stream", 21 | "text": [ 22 | "The autoreload extension is already loaded. To reload it, use:\n", 23 | " %reload_ext autoreload\n" 24 | ] 25 | } 26 | ], 27 | "source": [ 28 | "%load_ext autoreload\n", 29 | "%autoreload 2\n", 30 | "%matplotlib inline" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 166, 36 | "id": "6714aa7e-74b6-433b-a37d-fc44d3fb577e", 37 | "metadata": { 38 | "execution": { 39 | "iopub.execute_input": "2024-10-29T01:17:56.471464Z", 40 | "iopub.status.busy": "2024-10-29T01:17:56.471133Z", 41 | "iopub.status.idle": "2024-10-29T01:17:56.502029Z", 42 | "shell.execute_reply": "2024-10-29T01:17:56.501547Z", 43 | "shell.execute_reply.started": "2024-10-29T01:17:56.471444Z" 44 | }, 45 | "scrolled": true, 46 | "tags": [] 47 | }, 48 | "outputs": [], 49 | "source": [ 50 | "from utils.pp.preprocess import preprocess\n", 51 | "from utils.pl.plot_embedding import plot_embedding\n", 52 | "from utils.tl.transfer_labels import transfer_labels\n", 53 | "from utils.pl.plot_ternary import plot_kde\n", 54 | "from requirements import *" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "id": "5478eb79-6299-430e-bd33-908e5c60a512", 60 | "metadata": {}, 61 | "source": [ 62 | "## Import AnnDatas" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 95, 68 | "id": "a2aed545-d549-411a-9642-0b79be235082", 69 | "metadata": { 70 | "execution": { 71 | "iopub.execute_input": "2024-10-29T00:32:54.789647Z", 72 | "iopub.status.busy": "2024-10-29T00:32:54.789348Z", 73 | "iopub.status.idle": "2024-10-29T00:32:56.852337Z", 74 | "shell.execute_reply": "2024-10-29T00:32:56.851764Z", 75 | "shell.execute_reply.started": "2024-10-29T00:32:54.789627Z" 76 | }, 77 | "tags": [] 78 | }, 79 | "outputs": [], 80 | "source": [ 81 | "# Read in KG146 tumor data for mapping\n", 82 | "filepath = f'{data_dir}/h5ads/KG146_Tumor_Mapping_Reference.h5ad'\n", 83 | "ad_146 = sc.read_h5ad(filepath, backed=False)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 96, 89 | "id": "ebd3cead-acac-4de4-a58c-7517face7c56", 90 | "metadata": { 91 | "execution": { 92 | "iopub.execute_input": "2024-10-29T00:32:56.853614Z", 93 | "iopub.status.busy": "2024-10-29T00:32:56.853340Z", 94 | "iopub.status.idle": "2024-10-29T00:32:58.799914Z", 95 | "shell.execute_reply": "2024-10-29T00:32:58.799351Z", 96 | "shell.execute_reply.started": "2024-10-29T00:32:56.853593Z" 97 | }, 98 | "tags": [] 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "# Read in KG146 organoid data\n", 103 | "filepath = f'{data_dir}/h5ads/KG146_shPROX1_Knockdown.h5ad'\n", 104 | "ad_146_kd = sc.read_h5ad(filepath, backed=False)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "id": "5b87a459-dc5b-4810-9949-e79f192150ca", 110 | "metadata": { 111 | "execution": { 112 | "iopub.execute_input": "2024-10-28T23:50:13.027675Z", 113 | "iopub.status.busy": "2024-10-28T23:50:13.027119Z", 114 | "iopub.status.idle": "2024-10-28T23:50:13.054085Z", 115 | "shell.execute_reply": "2024-10-28T23:50:13.053599Z", 116 | "shell.execute_reply.started": "2024-10-28T23:50:13.027654Z" 117 | } 118 | }, 119 | "source": [ 120 | "## Figure 4a. Label mapping from patient data to shPROX1 knockdown organoids" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "id": "5ff70ddc-3aa5-4c29-aaaf-0f17868dc57a", 126 | "metadata": {}, 127 | "source": [ 128 | "### Get genes to use as feature space for mapping" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 98, 134 | "id": "231386eb-d3f9-4e29-830c-d608f575fb8d", 135 | "metadata": { 136 | "execution": { 137 | "iopub.execute_input": "2024-10-29T00:33:08.976110Z", 138 | "iopub.status.busy": "2024-10-29T00:33:08.975794Z", 139 | "iopub.status.idle": "2024-10-29T00:33:12.624017Z", 140 | "shell.execute_reply": "2024-10-29T00:33:12.623294Z", 141 | "shell.execute_reply.started": "2024-10-29T00:33:08.976091Z" 142 | } 143 | }, 144 | "outputs": [], 145 | "source": [ 146 | "# Get DEGs per cell state\n", 147 | "sc.tl.rank_genes_groups(\n", 148 | " ad_146,\n", 149 | " \"cell_state\",\n", 150 | " layer='log',\n", 151 | " use_raw=False,\n", 152 | " method='wilcoxon',\n", 153 | ")" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 99, 159 | "id": "7c7aba72-a060-4236-822b-848635747b1c", 160 | "metadata": { 161 | "execution": { 162 | "iopub.execute_input": "2024-10-29T00:33:12.625646Z", 163 | "iopub.status.busy": "2024-10-29T00:33:12.625332Z", 164 | "iopub.status.idle": "2024-10-29T00:33:12.689923Z", 165 | "shell.execute_reply": "2024-10-29T00:33:12.689434Z", 166 | "shell.execute_reply.started": "2024-10-29T00:33:12.625626Z" 167 | } 168 | }, 169 | "outputs": [], 170 | "source": [ 171 | "# Get features to use for cell state mapping\n", 172 | "n_genes = 200\n", 173 | "degs = ad_146.uns['rank_genes_groups']\n", 174 | "cell_states = dict()\n", 175 | "keys = list(degs['names'].dtype.fields.keys())\n", 176 | "for key in keys:\n", 177 | " genes = degs['names'][key]\n", 178 | " mask = degs['logfoldchanges'][key] > 1\n", 179 | " mask &= degs['pvals_adj'][key] < 0.001\n", 180 | " mask &= pd.Index(genes).isin(ad_146_kd.var.index)\n", 181 | " cell_states[key] = genes[mask][:n_genes].tolist()\n", 182 | "label_features = [v for vals in cell_states.values() for v in vals]" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "id": "860ac356-df9c-415d-b804-272ba560cbfd", 188 | "metadata": {}, 189 | "source": [ 190 | "### Perform label transfer on each organoid and condition" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 100, 196 | "id": "e4d97bbd-4a7b-4776-a4e4-2a13e0660343", 197 | "metadata": { 198 | "execution": { 199 | "iopub.execute_input": "2024-10-29T00:33:12.690865Z", 200 | "iopub.status.busy": "2024-10-29T00:33:12.690575Z", 201 | "iopub.status.idle": "2024-10-29T00:33:12.716547Z", 202 | "shell.execute_reply": "2024-10-29T00:33:12.716077Z", 203 | "shell.execute_reply.started": "2024-10-29T00:33:12.690846Z" 204 | } 205 | }, 206 | "outputs": [], 207 | "source": [ 208 | "# Columns to retain after co-embedding\n", 209 | "keep_cols = [\n", 210 | " 'sample_id',\n", 211 | " 'label_group',\n", 212 | " 'cell_state',\n", 213 | " 'original_line',\n", 214 | " 'genotype',\n", 215 | "]" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 101, 221 | "id": "e1425a56-4a52-4599-9e69-accde114bfcc", 222 | "metadata": { 223 | "execution": { 224 | "iopub.execute_input": "2024-10-29T00:33:13.674743Z", 225 | "iopub.status.busy": "2024-10-29T00:33:13.674394Z", 226 | "iopub.status.idle": "2024-10-29T00:35:18.230619Z", 227 | "shell.execute_reply": "2024-10-29T00:35:18.230018Z", 228 | "shell.execute_reply.started": "2024-10-29T00:33:13.674725Z" 229 | } 230 | }, 231 | "outputs": [ 232 | { 233 | "name": "stderr", 234 | "output_type": "stream", 235 | "text": [ 236 | "100%|██████████| 4/4 [02:04<00:00, 31.13s/it]\n" 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "from tqdm import tqdm\n", 242 | "\n", 243 | "# Reference and unlabeled group\n", 244 | "ad_146.obs['label_group'] = 'reference'\n", 245 | "ad_146_kd.obs['label_group'] = 'unlabeled'\n", 246 | "labeled_ad = dict()\n", 247 | "\n", 248 | "# Label transfer is performed separately for each sample\n", 249 | "for name, group in tqdm(ad_146_kd.obs.groupby(['original_line', 'genotype'])):\n", 250 | "\n", 251 | " # Coembed each sample with tumor AnnData (labeled reference)\n", 252 | " ad_cmb = anndata.concat(\n", 253 | " adatas=[ad_146, ad_146_kd[group.index]],\n", 254 | " join=\"outer\",\n", 255 | " label=\"label_group\",\n", 256 | " keys=[\"reference\", \"unlabeled\"],\n", 257 | " index_unique=None,\n", 258 | " )\n", 259 | " ad_cmb.obsm.clear()\n", 260 | " ad_cmb.obs = ad_cmb.obs[keep_cols]\n", 261 | "\n", 262 | " # Re-process data after co-embedding\n", 263 | " kwargs = dict(\n", 264 | " hvgs=label_features, show_progress=False,\n", 265 | " cluster=False, neighbors=False, umap=False, impute=False,\n", 266 | " )\n", 267 | " ad_cmb = preprocess(ad_cmb, **kwargs)\n", 268 | "\n", 269 | " # Transfer labels from tumor to organoid\n", 270 | " transfer_labels(ad_cmb, label_column='cell_state', knn=30, mnn=60)\n", 271 | " labeled_ad[name] = ad_cmb" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": 102, 277 | "id": "4b4ddd9a-b279-43ef-ba36-efde27d99455", 278 | "metadata": { 279 | "execution": { 280 | "iopub.execute_input": "2024-10-29T00:35:18.232073Z", 281 | "iopub.status.busy": "2024-10-29T00:35:18.231792Z", 282 | "iopub.status.idle": "2024-10-29T00:35:18.295231Z", 283 | "shell.execute_reply": "2024-10-29T00:35:18.294741Z", 284 | "shell.execute_reply.started": "2024-10-29T00:35:18.232054Z" 285 | } 286 | }, 287 | "outputs": [], 288 | "source": [ 289 | "# Transfer organoid labels back to original AnnData\n", 290 | "columns = [f'P({state})' for state in cell_states.keys()] # probabilities\n", 291 | "columns += ['cell_state'] # label\n", 292 | "\n", 293 | "for ad in labeled_ad.values():\n", 294 | " ixn = ad_146_kd.obs.index.intersection(ad.obs.index)\n", 295 | " ad_146_kd.obs.loc[ixn, columns] = ad.obs[columns]" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 103, 301 | "id": "e260fd56-4956-4cdb-b402-8f29c2ab9556", 302 | "metadata": { 303 | "execution": { 304 | "iopub.execute_input": "2024-10-29T00:35:18.296167Z", 305 | "iopub.status.busy": "2024-10-29T00:35:18.295989Z", 306 | "iopub.status.idle": "2024-10-29T00:35:18.328673Z", 307 | "shell.execute_reply": "2024-10-29T00:35:18.328203Z", 308 | "shell.execute_reply.started": "2024-10-29T00:35:18.296149Z" 309 | } 310 | }, 311 | "outputs": [], 312 | "source": [ 313 | "# Aggregate probabilities over 3 axes/groups of cell states\n", 314 | "label_map = {\n", 315 | " 'ISC/TA-like': [\"Proliferative\", \"ISC\"],\n", 316 | " 'Diff. Intestine-like': [\"Absorptive\", \"Secretory\"],\n", 317 | " 'Non-Canonical': [\"Injury Repair\", \"Neuroendocrine\", \"Fetal\", \"Squamous\"],\n", 318 | "}\n", 319 | "for key, vals in label_map.items():\n", 320 | " prob = ad_146_kd.obs[[f\"P({v})\" for v in vals]].sum(axis=1)\n", 321 | " ad_146_kd.obs[f\"P({key})\"] = prob" 322 | ] 323 | }, 324 | { 325 | "cell_type": "markdown", 326 | "id": "01ad518a-ad09-4042-bb3c-99f3578da12b", 327 | "metadata": {}, 328 | "source": [ 329 | "### Visualize results as ternary plots" 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": 170, 335 | "id": "43e8649c-06e7-4b57-a7b5-569fc5ea2702", 336 | "metadata": { 337 | "execution": { 338 | "iopub.execute_input": "2024-10-29T01:19:56.401370Z", 339 | "iopub.status.busy": "2024-10-29T01:19:56.401043Z", 340 | "iopub.status.idle": "2024-10-29T01:19:56.787875Z", 341 | "shell.execute_reply": "2024-10-29T01:19:56.787388Z", 342 | "shell.execute_reply.started": "2024-10-29T01:19:56.401347Z" 343 | } 344 | }, 345 | "outputs": [ 346 | { 347 | "data": { 348 | "image/png": "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dOWVV+73/Ne+9rXUp5B58+Yd8JJAT+bOnatEIiGXy6Wbb755vzFTRx11lK677jpJ0sqVK7Vhw4b+voyMlq42CgQCWrhwoSTp/PPP1znnnLPfMU6nU3feeadcLpck6bXXXuvXa8h06Xwf7cswDN12222KRCIaMWJEv8+TTdL9c87r9UqS7rnnHo0fP77b83a7Xb/4xS9S999+++0+158N0tlGH374oSTJZrPpZz/72X7PT506VZdeeqmk5Biu6urqPtefLRYvXqxvfvObWrFihaTk/++BsmJWMDW8lZeXa+fOnZKkCy+88IAD0jv/U3u93lSD9UZnF+ppp52msWPH9njMxRdfnPq+/FDbXzrbaNOmTQqHw5J00MsEo0aNUllZmSRp+/btva49W6T7fbSvZ555RqtXr9bIkSP1wx/+sN/nyRbpbp/58+dLSv6cO/3003s85sgjj9RVV12lq6++WtOnT+9L+Vkh3W3U0tIiSSopKTnghIejjz46dbupqanX584mN954o370ox+ptrZWNptN1113nU4++eQBn9eKWcHU8LZmzZrU7VNOOeWAx5100kmp2719w1RVVaUGwB/s3CNHjkzNIBnIL7RMlc42Gj16tK6//np961vf6vUsq0gk0qvjskk622hf9fX1+stf/iJJuummm1RSUtKv82STdLZPTU1NagD8rFmzDnrsr3/9a9122236+te/3qtzZ5N0v4fGjBkjSWptbVVbW1uPx1RVVaVul5aW9vrc2aRz/PP48eP15JNP6sYbbxzwOa2aFUzvees0adKkAx43atQo5eXlSVLq01Ffzj1x4sSDHtv5fG/PnU3S2UaTJ0/WDTfcoN/97ncHDW9erzc10HfcuHG9Onc2SWcb7es3v/mNfD6fzjjjDF100UX9Oke2SWf7dL4vJHXrUYvFYqqpqVFlZSUfeHoh3e+hrlcWuk7y6VRTU6N//vOfkqRjjz1Whx9+eK/PnU1KS0v1y1/+Um+99Za++MUvDso5rZoVTF3nraGhQZLkdrs1atSogx47duxYVVRUpL6mt+eWdMg3QuennLa2NkUiEdZC6iKdbdRbTz/9dGp8yZlnnjmo584EQ9VG8+fP16JFi5STk6M777yzX7Vmo3S2T9dfPIcffriam5t133336Y033pDP55Mk5eXladasWfrP//zPVA8Qukv3e+i8887T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0047bRBfVeZId/vceuutqb0xp0yZomuvvVZlZWWqq6vTk08+qU8//VRSMqD85je/SfOrtaZ0ttGyZcv07//+74rFYnI6nfrmN7+pc889VyNGjNDOnTv19NNPa8OGDZKSOy387//+b1p2E8hEV199tVasWKHx48dr0aJFPR6TiVnB9GtTDodDjzzyiL773e9q586deumll1Jd053Gjh2rxx57rF+DbP/rv/5LDQ0NWrBggVavXr3fNW23260//OEPw6Ixhqt0tdHzzz+fuu31evXtb3/7kF9zsDdoNkv3+wgDk+72+e1vfyubzabnn39eFRUVuv322/c75pJLLtEtt9zS79eQ6dLZRqeffrruv/9+/fKXv5Tf79dzzz2n5557br/jZs6cqT//+c8ENxNYLSuYHt6k5Bvi5Zdf1pw5c/Tmm29q165dikajOuKII/SVr3xF3/ve91RSUtKvc7tcLt1///2aP3++nn/+eW3atEl+v1+jRo3SGWecoX//93+nJ6cX0tFGjI0aXOl8H2Hg0tk+DodDd911ly688EI999xzWr16tZqbmzVq1Ch97nOf0xVXXKGzzz57cF9QBkpnG5177rl6++23NWfOHC1ZskS7d+9WOBzWyJEjdfzxx+uSSy5hPKKJrJYVTL9sCgAAgN6jbxYAAMBCCG8AAAAWQngDAACwEMIbAACAhRDeAAAALITwBgAAYCGENwAAAAshvAEAAFgI4Q0AAMBCCG8AAAAWQngDAACwEMIbAACAhRDeAAAALITwBgAAYCGENwAAAAshvAEAAFgI4Q0AAMBCCG8AAAAWQngDAACwEMIbAACAhRDeAAAALMRpdgFAT6qrq3Xuuef26libzSaXy6W8vDyNGTNG06ZN02mnnaZZs2apoKCgT9/3lltu0QsvvKDp06frn//8p+z2zz7f3HTTTXrppZdS9++9917NmjWrT+eXpHPOOUc1NTWSpB/96Ef6z//8zz6fIxNVVVXpnXfe0SeffKLt27erpaVFoVBIBQUFGjNmjKZPn66zzjpL559/vjwej9nlZpV934/vvvuuJkyYYGJFffPiiy/q5ptvliSNHz9eixYt6vZ8MBjUrFmztGfPHl1//fW64YYbzCgT6DV63mB5hmEoEomora1N27dv12uvvabbbrtN5557rp577rlen2f+/Pl64YUXZLPZdPvtt3cLbj357W9/q5aWloGWn/W2bNmiH//4xzrvvPN0zz336N1331VlZaV8Pp9isViqXV9++WX94he/0Je+9CXNmzdP8Xjc7NKRIXJzc/Vf//VfkqTZs2dr1apVJlcEHBw9b7CEsrIylZaWHvD5SCQin8+n3bt3y+/3S5La2tp0++23q6mpST/5yU8Oev7GxkbdcccdkqSvf/3rOv744w9ZU0tLi+68807dd999vX8hSDEMQw8++KAeeeQRxWKxbs+NGzdOhx12mDwej7xer8rLyxWJRCQl2/W3v/2tFi1apPvuu6/PvatAT2bNmqW5c+dq5cqV+uUvf6k33nhDubm5ZpcF9IjwBku49tprdemllx7yuEgkon/84x/64x//qHA4LEl68MEHdeaZZx40kN19991qb2+Xx+PRz372s17X9dZbb+mtt97Sv/7rv/b6ayAlEgn96le/0muvvZZ6rKSkRN///vf1jW98Q2PHju12fCQS0cKFC3X//fdr9+7dkqQPP/xQ3//+9zVnzhy53e4hrR+Z6aabbtK3vvUt1dTU6IEHHtCvfvUrs0sCesRlU2QUt9utq666Sn/5y19SjyUSCT3wwAMH/JqlS5fqjTfekCRdeeWVGjduXJ++55133snl0z76y1/+0i24nXXWWVqwYIF+8IMf7BfcpGS7XnjhhXrllVd09tlnpx5fs2aN/vCHPwxJzdlswoQJ2rp1a+qPlca79cVxxx2n888/X5L097//XTt27DC5IqBnhDdkpK985Ss6/fTTU/eXLl16wIDVGfScTqeuvfbaXp3f6fys07qlpUW//e1vB1BtdlmzZo0ef/zx1P2zzjpLjz76qEpKSg75tbm5ubrvvvt09NFHpx579tlntXXr1nSUiiz0gx/8QJIUi8UYEoFhi/CGjNX5CVqS4vF4j5+i3333Xa1bt06S9OUvf7nHXp+eHHfccTr11FNT9998800tWLBggBVnh7vvvluGYUhKXir905/+1C0MH0pubq5uueWW1P14PK45c+YMep3ITjNmzNDnP/95SdLChQu1ceNGkysC9seYN2SsfS/tNDY27nfMU089lbp92WWX9frcNptNv//97/WNb3xDgUBAUvLy6SmnnKKRI0f2r+ADiEQiev311/Xee+9pw4YNqR7EkSNH6thjj9XZZ5+tr3/964cc93XMMcdISgam5cuXS0rO9HzxxRf18ccfq76+XpFIRKWlpTr55JN1ySWXdAuog2H16tX69NNPU/evvvrqfv17nXnmmTrmmGO0bds2TZ06VSNGjDjo8X6/X2+++aaWLVumjRs3qrW1VR0dHcrNzVVRUZGOOeYYnXHGGbrkkksOOgHi6quv1ooVKyRJr732msrKytTY2KgXX3xR7777rqqrq9Xe3q5Ro0bpc5/7nGbNmqULL7xQDoejV69ruLZ1X5cKiUajWrhwoRYsWKANGzaosbFRdrtdo0aN0vTp0zVr1iydf/75vQrt69at07vvvquVK1eqtrZWbW1tisViKioq0tixY3XiiSdq1qxZOumkkw55rt667LLL9Jvf/EZS8vLpH//4x0E7NzAYCG/IWNFotNv9fWeObd++PfWLuKCgQGeccUafzn/EEUfoZz/7me666y5JUnNzs37729/q3nvv7X/R+3j33Xd1xx139Bg8a2pqVFNTowULFujBBx/U7bffrpkzZ/bqvIlEQvfee68ef/zx/ZbcqKysVGVlpV588UV99atf1R/+8IdBmxDw5ptvdrt/0UUX9ftcDz74oEpKSlRUVHTQ45566inNnj1bbW1t+z3X0dGhjo4O1dTUaNGiRXrggQd01113deu1PZj58+frN7/5jbxeb7fH6+rqVFdXp8WLF+uJJ57QI488osMPP/yg58qUtv7oo490++23q7q6+oCvY+HChTrmmGP0l7/8pdsl8K6qqqr061//WkuXLu3x+aamJjU1NWnjxo16+umnNXPmTP35z38elNnH559/vu68804ZhqE333xTN91006B/KAMGgsumyFj7joPat6fg1VdfTd0+88wz5XK5+vw9rrrqKp1yyimp+4N5+fTJJ5/Uj3/8426/zEtKSnTCCSfoxBNP7DZGrLa2Vj/+8Y/197//vVfn/v3vf69HH31U8XhcDodD06ZN06mnnqojjjii23Hz58/Xr3/960F5PVLyF3un8ePH7/f9+mLixImHDG533XWX7r777lRws9lsmjx5sk499VSddtppmjJlSrf1/Lxer376059q7dq1h/z+Cxcu1M9//vNUcJs4caJOO+00HXPMMd3OuXXrVl177bWppU56kilt/fLLL+v73/9+t+BWWFio448/Xl/4whe6BautW7fqqquuUkVFxX7nKS8v1+WXX94tuBUWFuoLX/iCzjjjDB1//PH7tf3ixYv7NFP8YDp7CKVkb+i+HzoAs9HzhowUjUa77YZQWlqqsrKybse88847qdtnnnlmv75P18unwWBQ0uBcPn3rrbe6zaIcN26cbrnlFp177rmpS3DxeFxvv/22fve736mhoUGJREJ33323Jk+e3G1G5r7a2tr09NNPy2az6aqrrtJ1112nUaNGpZ5fuXKlfvWrX6V2gXjllVf0gx/84IA9JL0VDoe1a9eu1P3BvMzVkyVLlujpp59O3b/gggt088037zebuL6+Xg8//LD+7//+T1Kyp2r27Nl69NFHD3r+zhnMZ555pm666aZu/7+qq6t12223pcLHrl279M9//lNXXnnlfufJlLbetGmTbrvtNiUSCUnSiBEjdNNNN+lrX/ta6oNRJBLR3Llz9T//8z+KxWJqbW3Vr371Kz3//POp8xiGoVtvvVXNzc2SpKKiIv32t7/Veeed1+0yayKR0Hvvvaf//u//Vm1trSTp/fff1/r163Xcccf1uf59nXnmmdqwYYOkZK/ot7/97QGfExgs9Lwh40SjUd1+++2qqqpKPXb11Vd3O6ahoUE7d+5M3Z82bVq/v9/EiRO7feJvbm7Wf//3f/f7fJFIRPfcc09qUP8RRxyh5557Tueff363sVMOh0P/+q//queeey51Sc4wDN12220H7eXpdNNNN+m2227r9stckk4++WQ9+uijqV+UhmHo7bff7vfr6VRTU9Ptst2hLiMO1GOPPZa6fcIJJ+jee+/tcRmYsWPH6s4779R5552Xeuzjjz/u1Q4OX/nKV/TEE0/s98FgwoQJevTRRzV+/PjUYz39G2ZSW//xj39MDVUoKirSs88+q4svvrhbj7bb7db3vvc93XbbbanH1q1b161HdsWKFVqzZk3q/p///GfNmjVrv/Fxdrtd55xzjmbPnt3t8Q8//LBf9e+r68+EVatW7TcMAzAT4Q2Wl0gk1NHRoS1btmjevHm69NJL9eKLL6aeP/roo/cLb10HzdtstgH3Kl199dXdLp/Onz+/35dPX3nlFe3Zsyd1/+677z7oLNjDDz9cv//971P3Gxoa9Morrxz0e0yaNEnf+c53Dvj81KlTu/WMbdu2rTelH5TP5+t2P51jiHw+X6rXRJL+3//7f4fc7uyCCy5I3Y5EImptbT3o8Xa7XbfccotsNluPz3s8nm4LS/f0b5gpbV1RUdHtEufPf/5zTZky5YDHX3HFFZo8eXLq/sKFC1O3u55n+vTpB+1ZlJIhq+u56urq+lD5gXVO+pCkUCikLVu2DMp5gcHAZVNYws0335zaWLovJk6cqIceemi/yQpdlw0ZP3688vPzB1TfYF4+XbJkSer29OnTu4XCA/niF7+o6dOnp5Y1WLRokf7t3/7tgMd/5StfOWDo6DR16tTUTMX29vbelH5Q+/YQpXProYKCAq1evVoNDQ2qrKzUF77whUN+zejRo7vdD4VCBz1++vTp3XrWejJ16tTU7Z7+DTOlrRcvXpy6nZeX16uJKDfddJNqa2s1efLkbh+ebrzxRv2///f/VFVV1etZuqNHj05dku/cWWWgJk+eLLfbnfp/W15ePiiXY4HBQM8bMlJRUZG+853v6MUXX+z2qbxT1wHV+15K6q/Buny6cuXK1O2zzjqr11/Xddze6tWrD3ps116FAyksLEzdHoxLRvuGtZ5mfw4mm82msWPH6pRTTjnoDEqfz6elS5d2m8AiKTV260B682/YdVB9T/+GmdLWnWslStKxxx7bq2A+c+ZMffvb39aZZ565X29jbm6uysrKdNRRRx30HBUVFXr++ee7DZHozeXu3nA4HCouLk7d72n2LGAWet5gCQfbmN7pdCo/P18FBQWaOHGipk+fruOPP/6gv0C6XhLr+otroK6++motWLAg9Ut5/vz5qTWteiMWi6UGaks65C+vrrr28rS1tSkSiRwwtPRmN4OuvR6dY7IGYt+QfKjLkoOtrq5OW7ZsUWVlpaqqqlRVVaXy8nJVV1f3GNQO9Zp782/Y9VLtvufLpLbuGp4Odrm0PyKRiLZs2aKKiopUu+3evVvbt2/f71K8NDj/VzsVFhamZgAP9f9X4GAIb7CE3m5M31udlzYlDfiSaVedl08vuuii1Pf4zW9+o5NPPrlXl0/3XS+s6yf/Q9n32NbW1gOOn8rJyen1eXvy4osv9uoy9qmnnpqa8Tl27Fjl5uam/l3q6+sHVENvRCIRPffcc3ruuee0ffv2gx7rdDoVi8V6fe6B/htapa17o2uIGox11qTk2LtHH31U7777brf3a0/62na91fWDXedi3MBwwGVTZKWuY4B6O66mtyZNmqT//M//TN3vy+XTgfQa7PvLa7Bf10DZbDYde+yxqfudCyT3V2Njo1566aXUMhH7amho0OWXX6677rqrx+A2YsQInXbaafr+97+vxx57TI888siA6umrTG7rgZo3b54uvvhivf766/sFN5fLpaOOOkoXXnihbr/9di1cuFAnnnhiWuro2nN6qAkvwFCi5w1ZqWtv26E+1fdH5+XTVatWSer95dN9Fx7dt3fmYPYdaD6YPYqD5fTTT9cnn3wiKRm+tm3btt8yG73VuSOBlBxveM8996RmTcZiMd1www3atGlT6vgTTjhBF1xwgY477jgdddRR+22p9fHHH/erjv7KpLbu2hPY06XMvli8eLF++9vfpu7n5+froosu0qmnnqpjjjlGEydO3G/ZkHT0uknp66EHBorwhqzUdRxQX35p9pbdbtfdd9+93+XTQ80mdLvdGjVqVGosVNdZsYfSdYmHkSNHpnU256WXXtqvy9hf+9rXUovbSskFavsb3rpOMKitrdWRRx6Zuv/222932yXhpz/9qX784x8f9HzpnkCxL6u0dW90vWTbdSHmg6mvr9eHH36oI444QhMmTEitX/c///M/qWPGjx+vZ555RocddthBz5WO9/C+5+3LZW0g3egHRlaaOHFi6vaBLrsN1KRJk3TjjTem7vf28mnXS0BdFy89lK7Hfv7zn+/11w2lKVOm6PTTT0/dnzt3bmrz9b5YsWJFqldTkr785S9360l7//33U7fHjh2rH/3oR4c8577reB1qtulgyJS27roUy4YNG3q1XMeSJUt0yy236Oqrr9Yll1wiwzBUXV3d7RL3D3/4w0MGt0AgoMrKytT9wZqwEIvF1NDQkLo/2BMxgIEgvCErdZ3Z19DQkJZLp5J0zTXXdFsA9Y033jjkIqJdFyXduHFjr8aGffzxx932cj3UwqZm+o//+I/Uba/Xq5tuuqlPl728Xq9uvfXW1H2bzaYbbrih2zFd9wgdNWrUIccrtbe3d9tOTRq8JScOJlPa+l/+5V9St/1+v+bPn3/Ir+l6zGmnnSabzdat3SRpzJgxhzzPM8880215k8G6hFpdXd3t/0DXnl3AbIQ3ZKXjjz8+NWkhkUho8+bNafk+drtdv//977vN+DtUKLjooou6LRh7yy23dOsB2Fd9fX237YaKiop08cUX97/oNDvppJN0xRVXpO6///77+o//+I9eLQ5bX1+va6+9tltPy1VXXbXf9mZdL4tv3779oDNbw+Gwbrrp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XuvXa8h06Xwf7cswDN12222KRCIaMWJEv8+TTdL9c87r9UqS7rnnHo0fP77b83a7Xb/4xS9S999+++0+158N0tlGH374oSTJZrPpZz/72X7PT506VZdeeqmk5Biu6urqPtefLRYvXqxvfvObWrFihaTk/++BsmJWMDW8lZeXa+fOnZKkCy+88IAD0jv/U3u93lSD9UZnF+ppp52msWPH9njMxRdfnPq+/FDbXzrbaNOmTQqHw5J00MsEo0aNUllZmSRp+/btva49W6T7fbSvZ555RqtXr9bIkSP1wx/+sN/nyRbpbp/58+dLSv6cO/3003s85sgjj9RVV12lq6++WtOnT+9L+Vkh3W3U0tIiSSopKTnghIejjz46dbupqanX584mN954o370ox+ptrZWNptN1113nU4++eQBn9eKWcHU8LZmzZrU7VNOOeWAx5100kmp2719w1RVVaUGwB/s3CNHjkzNIBnIL7RMlc42Gj16tK6//np961vf6vUsq0gk0qvjskk622hf9fX1+stf/iJJuummm1RSUtKv82STdLZPTU1NagD8rFmzDnrsr3/9a9122236+te/3qtzZ5N0v4fGjBkjSWptbVVbW1uPx1RVVaVul5aW9vrc2aRz/PP48eP15JNP6sYbbxzwOa2aFUzvees0adKkAx43atQo5eXlSVLq01Ffzj1x4sSDHtv5fG/PnU3S2UaTJ0/WDTfcoN/97ncHDW9erzc10HfcuHG9Onc2SWcb7es3v/mNfD6fzjjjDF100UX9Oke2SWf7dL4vJHXrUYvFYqqpqVFlZSUfeHoh3e+hrlcWuk7y6VRTU6N//vOfkqRjjz1Whx9+eK/PnU1KS0v1y1/+Um+99Za++MUvDso5rZoVTF3nraGhQZLkdrs1atSogx47duxYVVRUpL6mt+eWdMg3QuennLa2NkUiEdZC6iKdbdRbTz/9dGp8yZlnnjmo584EQ9VG8+fP16JFi5STk6M777yzX7Vmo3S2T9dfPIcffriam5t133336Y033pDP55Mk5eXladasWfrP//zPVA8Qukv3e+i8887TzJkztXjxYs2dO1d1dXW6+OKLNWLECG3cuFGzZ8+Wz+dTYWEh762DeOaZZwZljFtXVs0Kpoa39vZ2SUp9kjmYzmM6Ojp6de7OAby9OX/X5zs6Og755s0m6Wyj3tiyZYse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", 349 | "text/plain": [ 350 | "
" 351 | ] 352 | }, 353 | "metadata": {}, 354 | "output_type": "display_data" 355 | } 356 | ], 357 | "source": [ 358 | "plot_kde(\n", 359 | " ad_146_kd.obs,\n", 360 | " row='original_line',\n", 361 | " row_order=['OKG146P', 'OKG146Li'],\n", 362 | " x='P(Non-Canonical)',\n", 363 | " y='genotype',\n", 364 | " hue='sample_type',\n", 365 | " palette=named_colors,\n", 366 | ")" 367 | ] 368 | } 369 | ], 370 | "metadata": { 371 | "kernelspec": { 372 | "display_name": "Python 3 (ipykernel)", 373 | "language": "python", 374 | "name": "python3" 375 | }, 376 | "language_info": { 377 | "codemirror_mode": { 378 | "name": "ipython", 379 | "version": 3 380 | }, 381 | "file_extension": ".py", 382 | "mimetype": "text/x-python", 383 | "name": "python", 384 | "nbconvert_exporter": "python", 385 | "pygments_lexer": "ipython3", 386 | "version": "3.8.1" 387 | } 388 | }, 389 | "nbformat": 4, 390 | "nbformat_minor": 5 391 | } 392 | --------------------------------------------------------------------------------