From 0f888e840cdf8e9275999c1684ceb0f270a7c3c9 Mon Sep 17 00:00:00 2001 From: Altana Namsaraeva <99650244+namsaraeva@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:09:27 +0100 Subject: [PATCH] Harmonize plots (#574) * add scgen reg plot and fix typo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add changes to coda * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix pre commit * fix pyplot import * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix pre-commit * mixscape * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix * fix2 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix3 * milo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * enrichment * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * dialogue * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * augur * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scgen * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix pre-commit * precomm fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update pertpy/tools/_coda/_base_coda.py Co-authored-by: Lukas Heumos * adressed feedback * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix precommit --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lukas Heumos --- .../docstring_previews/scgen_reg_mean.png | Bin 0 -> 33174 bytes pertpy/plot/_augur.py | 20 +-- pertpy/plot/_coda.py | 6 +- pertpy/tools/_augur.py | 68 ++++++-- pertpy/tools/_coda/_base_coda.py | 78 +++++---- pertpy/tools/_dialogue.py | 33 +++- pertpy/tools/_enrichment.py | 10 +- pertpy/tools/_milo.py | 46 +++++- pertpy/tools/_mixscape.py | 94 ++++++++--- pertpy/tools/_scgen/_scgen.py | 156 +++++++++--------- 10 files changed, 344 insertions(+), 167 deletions(-) create mode 100644 docs/_static/docstring_previews/scgen_reg_mean.png diff --git a/docs/_static/docstring_previews/scgen_reg_mean.png b/docs/_static/docstring_previews/scgen_reg_mean.png new file mode 100644 index 0000000000000000000000000000000000000000..2b76a5ca1df98357cf8d0c791b133c9ec15c67e7 GIT binary patch literal 33174 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 Figure | Axes: + def dp_scatter(results: pd.DataFrame, top_n=None, ax: Axes = None) -> Figure | Axes: """Plot result of differential prioritization. Args: @@ -56,11 +56,11 @@ def dp_scatter(results: pd.DataFrame, top_n=None, ax: Axes = None, return_figure ag = Augur("random_forest_classifier") - return ag.plot_dp_scatter(results=results, top_n=top_n, ax=ax, return_figure=return_figure) + return ag.plot_dp_scatter(results=results, top_n=top_n, ax=ax) @staticmethod def important_features( - data: dict[str, Any], key: str = "augurpy_results", top_n=10, ax: Axes = None, return_figure: bool = False + data: dict[str, Any], key: str = "augurpy_results", top_n=10, ax: Axes = None ) -> Figure | Axes: """Plot a lollipop plot of the n features with largest feature importances. @@ -95,12 +95,10 @@ def important_features( ag = Augur("random_forest_classifier") - return ag.plot_important_features(data=data, key=key, top_n=top_n, ax=ax, return_figure=return_figure) + return ag.plot_important_features(data=data, key=key, top_n=top_n, ax=ax) @staticmethod - def lollipop( - data: dict[str, Any], key: str = "augurpy_results", ax: Axes = None, return_figure: bool = False - ) -> Figure | Axes: + def lollipop(data: dict[str, Any], key: str = "augurpy_results", ax: Axes = None) -> Figure | Axes | None: """Plot a lollipop plot of the mean augur values. Args: @@ -133,12 +131,10 @@ def lollipop( ag = Augur("random_forest_classifier") - return ag.plot_lollipop(data=data, key=key, ax=ax, return_figure=return_figure) + return ag.plot_lollipop(data=data, key=key, ax=ax) @staticmethod - def scatterplot( - results1: dict[str, Any], results2: dict[str, Any], top_n=None, return_figure: bool = False - ) -> Figure | Axes: + def scatterplot(results1: dict[str, Any], results2: dict[str, Any], top_n=None) -> Figure | Axes: """Create scatterplot with two augur results. Args: @@ -172,4 +168,4 @@ def scatterplot( ag = Augur("random_forest_classifier") - return ag.plot_scatterplot(results1=results1, results2=results2, top_n=top_n, return_figure=return_figure) + return ag.plot_scatterplot(results1=results1, results2=results2, top_n=top_n) diff --git a/pertpy/plot/_coda.py b/pertpy/plot/_coda.py index 9ff2d688..1464f670 100644 --- a/pertpy/plot/_coda.py +++ b/pertpy/plot/_coda.py @@ -429,8 +429,7 @@ def draw_tree( # pragma: no cover show=show, save=save, units=units, - h=h, - w=w, + figsize=(w, h), dpi=dpi, ) @@ -517,8 +516,7 @@ def draw_effects( # pragma: no cover show=show, save=save, units=units, - h=h, - w=w, + figsize=(w, h), dpi=dpi, ) diff --git a/pertpy/tools/_augur.py b/pertpy/tools/_augur.py index efc60b19..ac135fbf 100644 --- a/pertpy/tools/_augur.py +++ b/pertpy/tools/_augur.py @@ -976,8 +976,13 @@ def predict_differential_prioritization( return delta def plot_dp_scatter( - self, results: pd.DataFrame, top_n: int = None, ax: Axes = None, return_figure: bool = False - ) -> Figure | Axes: + self, + results: pd.DataFrame, + top_n: int = None, + ax: Axes = None, + show: bool | None = None, + save: str | bool | None = None, + ) -> Figure | Axes | None: """Plot scatterplot of differential prioritization. Args: @@ -1034,7 +1039,14 @@ def plot_dp_scatter( legend1 = ax.legend(*scatter.legend_elements(), loc="center left", title="z-scores", bbox_to_anchor=(1, 0.5)) ax.add_artist(legend1) - return fig if return_figure else ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax def plot_important_features( self, @@ -1042,8 +1054,9 @@ def plot_important_features( key: str = "augurpy_results", top_n: int = 10, ax: Axes = None, - return_figure: bool = False, - ) -> Figure | Axes: + show: bool | None = None, + save: str | bool | None = None, + ) -> Figure | Axes | None: """Plot a lollipop plot of the n features with largest feature importances. Args: @@ -1095,11 +1108,23 @@ def plot_important_features( plt.ylabel("Gene") plt.yticks(y_axes_range, n_features["genes"]) - return fig if return_figure else ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax def plot_lollipop( - self, data: dict[str, Any], key: str = "augurpy_results", ax: Axes = None, return_figure: bool = False - ) -> Figure | Axes: + self, + data: dict[str, Any], + key: str = "augurpy_results", + ax: Axes = None, + show: bool | None = None, + save: str | bool | None = None, + ) -> Figure | Axes | None: """Plot a lollipop plot of the mean augur values. Args: @@ -1147,11 +1172,23 @@ def plot_lollipop( plt.ylabel("Cell Type") plt.yticks(y_axes_range, results["summary_metrics"].sort_values("mean_augur_score", axis=1).columns) - return fig if return_figure else ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax def plot_scatterplot( - self, results1: dict[str, Any], results2: dict[str, Any], top_n: int = None, return_figure: bool = False - ) -> Figure | Axes: + self, + results1: dict[str, Any], + results2: dict[str, Any], + top_n: int = None, + show: bool | None = None, + save: str | bool | None = None, + ) -> Figure | Axes | None: """Create scatterplot with two augur results. Args: @@ -1207,4 +1244,11 @@ def plot_scatterplot( plt.xlabel("Augur scores 1") plt.ylabel("Augur scores 2") - return fig if return_figure else ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax diff --git a/pertpy/tools/_coda/_base_coda.py b/pertpy/tools/_coda/_base_coda.py index 4738a7d3..e4a9f301 100644 --- a/pertpy/tools/_coda/_base_coda.py +++ b/pertpy/tools/_coda/_base_coda.py @@ -26,11 +26,14 @@ from scipy.cluster import hierarchy as sp_hierarchy if TYPE_CHECKING: + from collections.abc import Sequence + import numpyro as npy import toytree as tt from ete3 import Tree from jax._src.typing import Array from matplotlib.axes import Axes + from matplotlib.colors import Colormap config.update("jax_enable_x64", True) @@ -1185,11 +1188,11 @@ def plot_stacked_barplot( # pragma: no cover data: AnnData | MuData, feature_name: str, modality_key: str = "coda", - figsize: tuple[float, float] | None = None, - dpi: int | None = 100, palette: ListedColormap | None = cm.tab20, show_legend: bool | None = True, level_order: list[str] = None, + figsize: tuple[float, float] | None = None, + dpi: int | None = 100, ax: plt.Axes | None = None, show: bool | None = None, save: str | bool | None = None, @@ -1288,11 +1291,11 @@ def plot_effects_barplot( # pragma: no cover plot_facets: bool = True, plot_zero_covariate: bool = True, plot_zero_cell_type: bool = False, - figsize: tuple[float, float] | None = None, - dpi: int | None = 100, palette: str | ListedColormap | None = cm.tab20, level_order: list[str] = None, args_barplot: dict | None = None, + figsize: tuple[float, float] | None = None, + dpi: int | None = 100, ax: plt.Axes | None = None, show: bool | None = None, save: str | bool | None = None, @@ -1492,11 +1495,11 @@ def plot_boxplots( # pragma: no cover cell_types: list | None = None, args_boxplot: dict | None = None, args_swarmplot: dict | None = None, - figsize: tuple[float, float] | None = None, - dpi: int | None = 100, palette: str | None = "Blues", show_legend: bool | None = True, level_order: list[str] = None, + figsize: tuple[float, float] | None = None, + dpi: int | None = 100, ax: plt.Axes | None = None, show: bool | None = None, save: str | bool | None = None, @@ -1738,7 +1741,7 @@ def plot_rel_abundance_dispersion_plot( # pragma: no cover ax: plt.Axes | None = None, show: bool | None = None, save: str | bool | None = None, - ) -> plt.Axes: + ) -> plt.Axes | None: """Plots total variance of relative abundance versus minimum relative abundance of all cell types for determination of a reference cell type. If the count of the cell type is larger than 0 in more than abundant_threshold percent of all samples, the cell type will be marked in a different color. @@ -1858,12 +1861,11 @@ def plot_draw_tree( # pragma: no cover tight_text: bool | None = False, show_scale: bool | None = False, units: Literal["px", "mm", "in"] | None = "px", - h: float | None = None, - w: float | None = None, - dpi: int | None = 90, + figsize: tuple[float, float] | None = None, + dpi: int | None = 100, show: bool | None = True, save: str | bool | None = None, - ): + ) -> Tree | None: """Plot a tree using input ete3 tree object. Args: @@ -1882,11 +1884,9 @@ def plot_draw_tree( # pragma: no cover file_name: Path to the output image file. Valid extensions are .SVG, .PDF, .PNG. Output image can be saved whether show is True or not. Defaults to None. - units: Unit of image sizes. “px”: pixels, “mm”: millimeters, “in”: inches. - Defaults to "px". - h: Height of the image in units. Defaults to None. - w: Width of the image in units. Defaults to None. - dpi: Dots per inches. Defaults to 90. + units: Unit of image sizes. “px”: pixels, “mm”: millimeters, “in”: inches. Defaults to "px". + figsize: Figure size. Defaults to None. + dpi: Dots per inches. Defaults to 100. Returns: Depending on `show`, returns :class:`ete3.TreeNode` and :class:`ete3.TreeStyle` (`show = False`) or plot the tree inline (`show = False`) @@ -1931,10 +1931,11 @@ def my_layout(node): tree_style.show_leaf_name = False tree_style.layout_fn = my_layout tree_style.show_scale = show_scale + if save is not None: - tree.render(save, tree_style=tree_style, units=units, w=w, h=h, dpi=dpi) # type: ignore + tree.render(save, tree_style=tree_style, units=units, w=figsize[0], h=figsize[1], dpi=dpi) # type: ignore if show: - return tree.render("%%inline", tree_style=tree_style, units=units, w=w, h=h, dpi=dpi) # type: ignore + return tree.render("%%inline", tree_style=tree_style, units=units, w=figsize[0], h=figsize[1], dpi=dpi) # type: ignore else: return tree, tree_style @@ -1948,12 +1949,11 @@ def plot_draw_effects( # pragma: no cover show_leaf_effects: bool | None = False, tight_text: bool | None = False, show_scale: bool | None = False, + units: Literal["px", "mm", "in"] | None = "px", + figsize: tuple[float, float] | None = None, + dpi: int | None = 100, show: bool | None = True, save: str | None = None, - units: Literal["px", "mm", "in"] | None = "in", - h: float | None = None, - w: float | None = None, - dpi: int | None = 90, ): """Plot a tree with colored circles on the nodes indicating significant effects with bar plots which indicate leave-level significant effects. @@ -1975,10 +1975,9 @@ def plot_draw_effects( # pragma: no cover show: If True, plot the tree inline. If false, return tree and tree_style objects. Defaults to True. file_name: Path to the output image file. valid extensions are .SVG, .PDF, .PNG. Output image can be saved whether show is True or not. Defaults to None. - units: Unit of image sizes. “px”: pixels, “mm”: millimeters, “in”: inches. Default is "in". Defaults to "in". - h: Height of the image in units. Defaults to None. - w: Width of the image in units. Defaults to None. - dpi: Dots per inches. Defaults to 90. + units: Unit of image sizes. “px”: pixels, “mm”: millimeters, “in”: inches. Defaults to "px". + figsize: Figure size. Defaults to None. + dpi: Dots per inches. Defaults to 100. Returns: Depending on `show`, returns :class:`ete3.TreeNode` and :class:`ete3.TreeStyle` (`show = False`) @@ -2130,21 +2129,23 @@ def my_layout(node): tree2.render(save, tree_style=tree_style, units=units) if show: if not show_leaf_effects: - return tree2.render("%%inline", tree_style=tree_style, units=units, w=w, h=h, dpi=dpi) + return tree2.render("%%inline", tree_style=tree_style, units=units, w=figsize[0], h=figsize[1], dpi=dpi) else: if not show_leaf_effects: return tree2, tree_style def plot_effects_umap( # pragma: no cover self, - data: MuData, + mdata: MuData, effect_name: str | list | None, cluster_key: str, modality_key_1: str = "rna", modality_key_2: str = "coda", + color_map: Colormap | str | None = None, + palette: str | Sequence[str] | None = None, + ax: Axes = None, show: bool = None, save: str | bool | None = None, - ax: Axes = None, **kwargs, ) -> plt.Axes | None: """Plot a UMAP visualization colored by effect strength. @@ -2153,7 +2154,7 @@ def plot_effects_umap( # pragma: no cover (default is data['rna']) depending on the cluster they were assigned to. Args: - data: AnnData object or MuData object. + mudata: MuData object. effect_name: The name of the effect results in .varm of aggregated sample-level AnnData to plot cluster_key: The cluster information in .obs of cell-level AnnData (default is data['rna']). To assign cell types' effects to original cells. @@ -2207,8 +2208,8 @@ def plot_effects_umap( # pragma: no cover .. image:: /_static/docstring_previews/tasccoda_effects_umap.png """ # TODO: Add effect_name parameter and cluster_key and test the example - data_rna = data[modality_key_1] - data_coda = data[modality_key_2] + data_rna = mdata[modality_key_1] + data_coda = mdata[modality_key_2] if isinstance(effect_name, str): effect_name = [effect_name] for _, effect in enumerate(effect_name): @@ -2224,7 +2225,18 @@ def plot_effects_umap( # pragma: no cover else: vmax = max(data_rna.obs[effect].max() for _, effect in enumerate(effect_name)) - return sc.pl.umap(data_rna, color=effect_name, vmax=vmax, vmin=vmin, ax=ax, show=show, save=save, **kwargs) + return sc.pl.umap( + data_rna, + color=effect_name, + vmax=vmax, + vmin=vmin, + palette=palette, + color_map=color_map, + ax=ax, + show=show, + save=save, + **kwargs, + ) def get_a( diff --git a/pertpy/tools/_dialogue.py b/pertpy/tools/_dialogue.py index 02750629..d51e4a27 100644 --- a/pertpy/tools/_dialogue.py +++ b/pertpy/tools/_dialogue.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Literal import anndata as ad +import matplotlib.pyplot as plt import numpy as np import pandas as pd import scanpy as sc @@ -1069,7 +1070,10 @@ def plot_split_violins( celltype_key: str, split_which: tuple[str, str] = None, mcp: str = "mcp_0", - ) -> Axes: + ax: Axes | None = None, + save: bool | str | None = None, + show: bool | None = None, + ) -> Axes | None: """Plots split violin plots for a given MCP and split variable. Any cells with a value for split_key not in split_which are removed from the plot. @@ -1107,10 +1111,24 @@ def plot_split_violins( ax.set_xticklabels(ax.get_xticklabels(), rotation=90) - return ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax def plot_pairplot( - self, adata: AnnData, celltype_key: str, color: str, sample_id: str, mcp: str = "mcp_0" + self, + adata: AnnData, + celltype_key: str, + color: str, + sample_id: str, + mcp: str = "mcp_0", + show: bool | None = None, + save: bool | str | None = None, ) -> PairGrid: """Generate a pairplot visualization for multi-cell perturbation (MCP) data. @@ -1150,4 +1168,11 @@ def plot_pairplot( mcp_pivot = pd.concat([mcp_pivot, aggstats[color]], axis=1) ax = sns.pairplot(mcp_pivot, hue=color, corner=True) - return ax + if show: + plt.show() + return None + if save: + plt.savefig(save, bbox_inches="tight") + return None + elif not show or show is None: + return ax diff --git a/pertpy/tools/_enrichment.py b/pertpy/tools/_enrichment.py index f713af4d..724d2ab7 100644 --- a/pertpy/tools/_enrichment.py +++ b/pertpy/tools/_enrichment.py @@ -7,6 +7,7 @@ import pandas as pd import scanpy as sc from anndata import AnnData +from matplotlib.axes import Axes from scanpy.plotting import DotPlot from scanpy.tools._score_genes import _sparse_nanmean from scipy.sparse import issparse @@ -292,6 +293,9 @@ def plot_dotplot( categories: Sequence[str] = None, groupby: str = None, key: str = "pertpy_enrichment", + ax: Axes | None = None, + save: bool | str | None = None, + show: bool | None = None, **kwargs, ) -> DotPlot | dict | None: """Plots a dotplot by groupby and categories. @@ -369,7 +373,9 @@ def plot_dotplot( "var_group_labels": var_group_labels, } - return sc.pl.dotplot(enrichment_score_adata, groupby=groupby, swap_axes=True, **plot_args, **kwargs) + return sc.pl.dotplot( + enrichment_score_adata, groupby=groupby, swap_axes=True, ax=ax, save=save, show=show, **plot_args, **kwargs + ) def plot_gsea( self, @@ -413,5 +419,5 @@ def plot_gsea( n=n, interactive_plot=interactive_plot, ) - fig.suptitle(cluster) + fig.subtitle(cluster) fig.show() diff --git a/pertpy/tools/_milo.py b/pertpy/tools/_milo.py index 6f2eaf4a..0e55d3a6 100644 --- a/pertpy/tools/_milo.py +++ b/pertpy/tools/_milo.py @@ -17,6 +17,9 @@ if TYPE_CHECKING: from collections.abc import Sequence + from matplotlib.axes import Axes + from matplotlib.colors import Colormap + try: from rpy2.robjects import conversion, numpy2ri, pandas2ri from rpy2.robjects.packages import STAP, PackageNotInstalledError, importr @@ -703,6 +706,9 @@ def plot_nhood_graph( min_size: int = 10, plot_edges: bool = False, title: str = "DA log-Fold Change", + color_map: Colormap | str | None = None, + palette: str | Sequence[str] | None = None, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, **kwargs, @@ -774,6 +780,9 @@ def plot_nhood_graph( vmax=vmax, vmin=vmin, title=title, + color_map=color_map, + palette=palette, + ax=ax, show=show, save=save, **kwargs, @@ -785,6 +794,9 @@ def plot_nhood( ix: int, feature_key: str | None = "rna", basis: str = "X_umap", + color_map: Colormap | str | None = None, + palette: str | Sequence[str] | None = None, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, **kwargs, @@ -815,7 +827,17 @@ def plot_nhood( """ mdata[feature_key].obs["Nhood"] = mdata[feature_key].obsm["nhoods"][:, ix].toarray().ravel() sc.pl.embedding( - mdata[feature_key], basis, color="Nhood", size=30, title="Nhood" + str(ix), show=show, save=save, **kwargs + mdata[feature_key], + basis, + color="Nhood", + size=30, + title="Nhood" + str(ix), + color_map=color_map, + palette=palette, + ax=ax, + show=show, + save=save, + **kwargs, ) def plot_da_beeswarm( @@ -826,6 +848,8 @@ def plot_da_beeswarm( alpha: float = 0.1, subset_nhoods: list[str] = None, palette: str | Sequence[str] | dict[str, str] | None = None, + save: bool | str | None = None, + show: bool | None = None, ) -> None: """Plot beeswarm plot of logFC against nhood labels @@ -931,12 +955,23 @@ def plot_da_beeswarm( plt.legend(loc="upper left", title=f"< {int(alpha * 100)}% SpatialFDR", bbox_to_anchor=(1, 1), frameon=False) plt.axvline(x=0, ymin=0, ymax=1, color="black", linestyle="--") + if save: + plt.savefig(save, bbox_inches="tight") + return None + if show: + plt.show() + return None + elif not show or show is None: + return plt.gcf() + def plot_nhood_counts_by_cond( self, mdata: MuData, test_var: str, subset_nhoods: list[str] = None, log_counts: bool = False, + save: bool | str | None = None, + show: bool | None = None, ) -> None: """Plot boxplot of cell numbers vs condition of interest. @@ -972,3 +1007,12 @@ def plot_nhood_counts_by_cond( plt.xticks(rotation=90) plt.xlabel(test_var) + + if save: + plt.savefig(save, bbox_inches="tight") + return None + if show: + plt.show() + return None + elif not show or show is None: + return plt.gcf() diff --git a/pertpy/tools/_mixscape.py b/pertpy/tools/_mixscape.py index f87e00d5..d6393c51 100644 --- a/pertpy/tools/_mixscape.py +++ b/pertpy/tools/_mixscape.py @@ -4,11 +4,11 @@ from collections import OrderedDict from typing import TYPE_CHECKING, Literal +import matplotlib.pyplot as plt import numpy as np import pandas as pd import scanpy as sc import seaborn as sns -from matplotlib import pyplot as pl from scanpy import get from scanpy._settings import settings from scanpy._utils import _check_use_raw, sanitize_anndata @@ -24,6 +24,7 @@ from anndata import AnnData from matplotlib.axes import Axes + from matplotlib.colors import Colormap from scipy import sparse @@ -509,6 +510,7 @@ def plot_barplot( # pragma: no cover axis_title_size: int = 8, legend_title_size: int = 8, legend_text_size: int = 8, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, ): @@ -558,7 +560,7 @@ def plot_barplot( # pragma: no cover if not show: color_mapping = {"KO": "salmon", "NP": "lightgray", "NT": "grey"} unique_genes = NP_KO_cells["gene"].unique() - fig, axs = pl.subplots(int(len(unique_genes) / 5), 5, figsize=(25, 25), sharey=True) + fig, axs = plt.subplots(int(len(unique_genes) / 5), 5, figsize=(25, 25), sharey=True) for i, gene in enumerate(unique_genes): ax = axs[int(i / 5), i % 5] grouped_df = ( @@ -592,10 +594,13 @@ def plot_barplot( # pragma: no cover fontsize=legend_text_size, title_fontsize=legend_title_size, ) - pl.tight_layout() + plt.tight_layout() _utils.savefig_or_show("mixscape_barplot", show=show, save=save) - return ax + if not show or show is None: + return ax + else: + return None def plot_heatmap( # pragma: no cover self, @@ -608,6 +613,8 @@ def plot_heatmap( # pragma: no cover subsample_number: int | None = 900, vmin: float | None = -2, vmax: float | None = 2, + fontsize: int | None = 8, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, **kwds, @@ -660,6 +667,8 @@ def plot_heatmap( # pragma: no cover vmax=vmax, n_genes=20, groups=["NT"], + fontsize=fontsize, + ax=ax, show=show, save=save, **kwds, @@ -670,12 +679,15 @@ def plot_perturbscore( # pragma: no cover adata: AnnData, labels: str, target_gene: str, - mixscape_class="mixscape_class", - color="orange", + mixscape_class: str = "mixscape_class", + color: str = "orange", palette: dict[str, str] = None, split_by: str = None, - before_mixscape=False, + before_mixscape: bool = False, perturbation_type: str = "KO", + ax: Axes | None = None, + show: bool | None = None, + save: bool | str | None = None, ) -> None: """Density plots to visualize perturbation scores calculated by the `pt.tl.mixscape` function. @@ -727,7 +739,7 @@ def plot_perturbscore( # pragma: no cover palette = {gd: "#7d7d7d", target_gene: color} plot_dens = sns.kdeplot(data=perturbation_score, x="pvec", hue=labels, fill=False, common_norm=False) top_r = max(plot_dens.get_lines()[cond].get_data()[1].max() for cond in range(len(plot_dens.get_lines()))) - pl.close() + plt.close() perturbation_score["y_jitter"] = perturbation_score["pvec"] rng = np.random.default_rng() perturbation_score.loc[perturbation_score[labels] == gd, "y_jitter"] = rng.uniform( @@ -738,7 +750,7 @@ def plot_perturbscore( # pragma: no cover ) # If split_by is provided, split densities based on the split_by if split_by is not None: - sns.set(style="whitegrid") + sns.set_theme(style="whitegrid") g = sns.FacetGrid( data=perturbation_score, col=split_by, hue=split_by, palette=palette, height=5, sharey=False ) @@ -750,26 +762,37 @@ def plot_perturbscore( # pragma: no cover # If split_by is not provided, create a single plot else: - sns.set(style="whitegrid") + sns.set_theme(style="whitegrid") sns.kdeplot( data=perturbation_score, x="pvec", hue="gene_target", fill=True, common_norm=False, palette=palette ) sns.scatterplot( data=perturbation_score, x="pvec", y="y_jitter", hue="gene_target", palette=palette, s=10, alpha=0.5 ) - pl.xlabel("Perturbation score", fontsize=16) - pl.ylabel("Cell density", fontsize=16) - pl.title("Density Plot", fontsize=18) - pl.legend(title="gene_target", title_fontsize=14, fontsize=12) + plt.xlabel("Perturbation score", fontsize=16) + plt.ylabel("Cell density", fontsize=16) + plt.title("Density Plot", fontsize=18) + plt.legend(title="gene_target", title_fontsize=14, fontsize=12) sns.despine() + ax = plt.gca() + + if save: + plt.savefig(save, bbox_inches="tight") + return None + if show: + plt.show() + return None + elif not show or show is None: + return ax + # If before_mixscape is False, split densities based on mixscape classifications else: if palette is None: palette = {gd: "#7d7d7d", f"{target_gene} NP": "#c9c9c9", f"{target_gene} {perturbation_type}": color} plot_dens = sns.kdeplot(data=perturbation_score, x="pvec", hue=labels, fill=False, common_norm=False) top_r = max(plot_dens.get_lines()[i].get_data()[1].max() for i in range(len(plot_dens.get_lines()))) - pl.close() + plt.close() perturbation_score["y_jitter"] = perturbation_score["pvec"] rng = np.random.default_rng() gd2 = list( @@ -788,7 +811,7 @@ def plot_perturbscore( # pragma: no cover ) # If split_by is provided, split densities based on the split_by if split_by is not None: - sns.set(style="whitegrid") + sns.set_theme(style="whitegrid") g = sns.FacetGrid( data=perturbation_score, col=split_by, hue="mix", palette=palette, height=5, sharey=False ) @@ -800,7 +823,7 @@ def plot_perturbscore( # pragma: no cover # If split_by is not provided, create a single plot else: - sns.set(style="whitegrid") + sns.set_theme(style="whitegrid") sns.kdeplot( data=perturbation_score, x="pvec", @@ -813,12 +836,23 @@ def plot_perturbscore( # pragma: no cover sns.scatterplot( data=perturbation_score, x="pvec", y="y_jitter", hue="mix", palette=palette, s=10, alpha=0.5 ) - pl.xlabel("Perturbation score", fontsize=16) - pl.ylabel("Cell density", fontsize=16) - pl.title("Density", fontsize=18) - pl.legend(title="mixscape class", title_fontsize=14, fontsize=12) + plt.xlabel("Perturbation score", fontsize=16) + plt.ylabel("Cell density", fontsize=16) + plt.title("Density", fontsize=18) + plt.legend(title="mixscape class", title_fontsize=14, fontsize=12) sns.despine() + ax = plt.gca() + + if save: + plt.savefig(save, bbox_inches="tight") + return None + if show: + plt.show() + return None + elif not show or show is None: + return ax + def plot_violin( # pragma: no cover self, adata: AnnData, @@ -838,9 +872,9 @@ def plot_violin( # pragma: no cover xlabel: str = "", ylabel: str | Sequence[str] | None = None, rotation: float | None = None, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, - ax: Axes | None = None, **kwargs, ): """Violin plot using mixscape results. @@ -1011,7 +1045,7 @@ def plot_violin( # pragma: no cover show = settings.autoshow if show is None else show if hue is not None and stripplot is True: - pl.legend(handles, labels) + plt.legend(handles, labels) _utils.savefig_or_show("mixscape_violin", show=show, save=save) if not show: @@ -1031,6 +1065,9 @@ def plot_lda( # pragma: no cover perturbation_type: str | None = "KO", lda_key: str | None = "mixscape_lda", n_components: int | None = None, + color_map: Colormap | str | None = None, + palette: str | Sequence[str] | None = None, + ax: Axes | None = None, show: bool | None = None, save: bool | str | None = None, **kwds, @@ -1076,4 +1113,13 @@ def plot_lda( # pragma: no cover n_components = adata_subset.uns[lda_key].shape[1] sc.pp.neighbors(adata_subset, use_rep=lda_key) sc.tl.umap(adata_subset, n_components=n_components) - sc.pl.umap(adata_subset, color=mixscape_class, show=show, save=save, **kwds) + sc.pl.umap( + adata_subset, + color=mixscape_class, + palette=palette, + color_map=color_map, + show=show, + save=save, + ax=ax, + **kwds, + ) diff --git a/pertpy/tools/_scgen/_scgen.py b/pertpy/tools/_scgen/_scgen.py index befe202a..3fc42c3a 100644 --- a/pertpy/tools/_scgen/_scgen.py +++ b/pertpy/tools/_scgen/_scgen.py @@ -3,13 +3,13 @@ from typing import TYPE_CHECKING, Any import jax.numpy as jnp +import matplotlib.pyplot as plt import numpy as np import pandas as pd import scanpy as sc from adjustText import adjust_text from anndata import AnnData from jax import Array -from matplotlib import pyplot from scipy import stats from scvi import REGISTRY_KEYS from scvi.data import AnnDataManager @@ -373,20 +373,19 @@ def get_latent_representation( def plot_reg_mean_plot( self, adata, - condition_key, - axis_keys, - labels, - path_to_save="./reg_mean.pdf", - save=True, - gene_list=None, - show=False, - top_100_genes=None, - verbose=False, - legend=True, - title=None, - x_coeff=0.30, - y_coeff=0.8, - fontsize=14, + condition_key: str, + axis_keys: dict[str, str], + labels: dict[str, str], + save: str | bool | None = None, + gene_list: list[str] = None, + show: bool = False, + top_100_genes: list[str] = None, + verbose: bool = False, + legend: bool = True, + title: str = None, + x_coeff: float = 0.30, + y_coeff: float = 0.8, + fontsize: float = 14, **kwargs, ) -> tuple[float, float] | float: """Plots mean matching for a set of specified genes. @@ -423,11 +422,14 @@ def plot_reg_mean_plot( >>> eval_adata = data[data.obs['cell_type'] == 'CD4 T cells'].copy().concatenate(pred) >>> r2_value = scg.plot_reg_mean_plot(eval_adata, condition_key='label', axis_keys={"x": "pred", "y": "stim"}, \ labels={"x": "predicted", "y": "ground truth"}, save=False, show=True) + + Preview: + .. image:: /_static/docstring_previews/scgen_reg_mean.png """ import seaborn as sns - sns.set() - sns.set(color_codes=True) + sns.set_theme() + sns.set_theme(color_codes=True) diff_genes = top_100_genes stim = adata[adata.obs[condition_key] == axis_keys["y"]] @@ -463,11 +465,11 @@ def plot_reg_mean_plot( j = adata.var_names.tolist().index(i) x_bar = x[j] y_bar = y[j] - texts.append(pyplot.text(x_bar, y_bar, i, fontsize=11, color="black")) - pyplot.plot(x_bar, y_bar, "o", color="red", markersize=5) + texts.append(plt.text(x_bar, y_bar, i, fontsize=11, color="black")) + plt.plot(x_bar, y_bar, "o", color="red", markersize=5) # if "y1" in axis_keys.keys(): # y1_bar = y1[j] - # pyplot.text(x_bar, y1_bar, i, fontsize=11, color="black") + # plt.text(x_bar, y1_bar, i, fontsize=11, color="black") if gene_list is not None: adjust_text( texts, @@ -477,11 +479,11 @@ def plot_reg_mean_plot( force_static=(0.0, 0.0), ) if legend: - pyplot.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) if title is None: - pyplot.title("", fontsize=fontsize) + plt.title("", fontsize=fontsize) else: - pyplot.title(title, fontsize=fontsize) + plt.title(title, fontsize=fontsize) ax.text( max(x) - max(x) * x_coeff, max(y) - y_coeff * max(y), @@ -496,10 +498,10 @@ def plot_reg_mean_plot( fontsize=kwargs.get("textsize", fontsize), ) if save: - pyplot.savefig(f"{path_to_save}", bbox_inches="tight", dpi=100) + plt.savefig(save, bbox_inches="tight") if show: - pyplot.show() - pyplot.close() + plt.show() + plt.close() if diff_genes is not None: return r_value**2, r_value_diff**2 else: @@ -508,20 +510,19 @@ def plot_reg_mean_plot( def plot_reg_var_plot( self, adata, - condition_key, - axis_keys, - labels, - path_to_save="./reg_var.pdf", - save=True, - gene_list=None, - top_100_genes=None, - show=False, - legend=True, - title=None, - verbose=False, - x_coeff=0.30, - y_coeff=0.8, - fontsize=14, + condition_key: str, + axis_keys: dict[str, str], + labels: dict[str, str], + save: str | bool | None = None, + gene_list: list[str] = None, + top_100_genes: list[str] = None, + show: bool = False, + legend: bool = True, + title: str = None, + verbose: bool = False, + x_coeff: float = 0.3, + y_coeff: float = 0.8, + fontsize: float = 14, **kwargs, ) -> tuple[float, float] | float: """Plots variance matching for a set of specified genes. @@ -548,8 +549,8 @@ def plot_reg_var_plot( """ import seaborn as sns - sns.set() - sns.set(color_codes=True) + sns.set_theme() + sns.set_theme(color_codes=True) sc.tl.rank_genes_groups(adata, groupby=condition_key, n_genes=100, method="wilcoxon") diff_genes = top_100_genes @@ -580,13 +581,13 @@ def plot_reg_var_plot( start, stop, step = kwargs.get("range") ax.set_xticks(np.arange(start, stop, step)) ax.set_yticks(np.arange(start, stop, step)) - # _p1 = pyplot.scatter(x, y, marker=".", label=f"{axis_keys['x']}-{axis_keys['y']}") - # pyplot.plot(x, m * x + b, "-", color="green") + # _p1 = plt.scatter(x, y, marker=".", label=f"{axis_keys['x']}-{axis_keys['y']}") + # plt.plot(x, m * x + b, "-", color="green") ax.set_xlabel(labels["x"], fontsize=fontsize) ax.set_ylabel(labels["y"], fontsize=fontsize) if "y1" in axis_keys.keys(): y1 = np.asarray(np.var(real_stim.X, axis=0)).ravel() - _ = pyplot.scatter( + _ = plt.scatter( x, y1, marker="*", @@ -599,17 +600,17 @@ def plot_reg_var_plot( j = adata.var_names.tolist().index(i) x_bar = x[j] y_bar = y[j] - pyplot.text(x_bar, y_bar, i, fontsize=11, color="black") - pyplot.plot(x_bar, y_bar, "o", color="red", markersize=5) + plt.text(x_bar, y_bar, i, fontsize=11, color="black") + plt.plot(x_bar, y_bar, "o", color="red", markersize=5) if "y1" in axis_keys.keys(): y1_bar = y1[j] - pyplot.text(x_bar, y1_bar, "*", color="black", alpha=0.5) + plt.text(x_bar, y1_bar, "*", color="black", alpha=0.5) if legend: - pyplot.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) if title is None: - pyplot.title("", fontsize=12) + plt.title("", fontsize=12) else: - pyplot.title(title, fontsize=12) + plt.title(title, fontsize=12) ax.text( max(x) - max(x) * x_coeff, max(y) - y_coeff * max(y), @@ -625,25 +626,25 @@ def plot_reg_var_plot( ) if save: - pyplot.savefig(f"{path_to_save}", bbox_inches="tight", dpi=100) + plt.savefig(save, bbox_inches="tight") if show: - pyplot.show() - pyplot.close() + plt.show() + plt.close() if diff_genes is not None: return r_value**2, r_value_diff**2 else: return r_value**2 - def plot_plot_binary_classifier( + def plot_binary_classifier( self, - scgen, - adata, - delta, - ctrl_key, - stim_key, - path_to_save, - save=True, - fontsize=14, + scgen: SCGEN, + adata: AnnData | None, + delta: np.ndarray, + ctrl_key: str, + stim_key: str, + show: bool = False, + save: str | bool | None = None, + fontsize: float = 14, ) -> None: """Plots the dot product between delta and latent representation of a linear classifier. @@ -663,7 +664,7 @@ def plot_plot_binary_classifier( save: Specify if the plot should be saved or not. fontsize: Set the font size of the plot. """ - pyplot.close("all") + plt.close("all") adata = scgen._validate_anndata(adata) condition_key = scgen.adata_manager.get_state_registry(REGISTRY_KEYS.BATCH_KEY).original_key cd = adata[adata.obs[condition_key] == ctrl_key, :] @@ -676,21 +677,26 @@ def plot_plot_binary_classifier( dot_cd[ind] = np.dot(delta, vec) for ind, vec in enumerate(all_latent_stim): dot_sal[ind] = np.dot(delta, vec) - pyplot.hist( + plt.hist( dot_cd, label=ctrl_key, bins=50, ) - pyplot.hist(dot_sal, label=stim_key, bins=50) - pyplot.axvline(0, color="k", linestyle="dashed", linewidth=1) - pyplot.title(" ", fontsize=fontsize) - pyplot.xlabel(" ", fontsize=fontsize) - pyplot.ylabel(" ", fontsize=fontsize) - pyplot.xticks(fontsize=fontsize) - pyplot.yticks(fontsize=fontsize) - ax = pyplot.gca() + plt.hist(dot_sal, label=stim_key, bins=50) + plt.axvline(0, color="k", linestyle="dashed", linewidth=1) + plt.title(" ", fontsize=fontsize) + plt.xlabel(" ", fontsize=fontsize) + plt.ylabel(" ", fontsize=fontsize) + plt.xticks(fontsize=fontsize) + plt.yticks(fontsize=fontsize) + ax = plt.gca() ax.grid(False) if save: - pyplot.savefig(f"{path_to_save}", bbox_inches="tight", dpi=100) - pyplot.show() + plt.savefig(save, bbox_inches="tight") + return None + if show: + plt.show() + return None + elif not show or show is None: + return ax