Skip to content

Commit a0a81cc

Browse files
Add plot_disp_ests() (#1004)
* work in progress * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * test deprecated_arg() * final deprecation warning after testing * add deprecated_arg() to plot_volcano() * replace alpha with padj_threshold * small fixes during testing * keep old default value * add warning filter for milo deprecation * add plot_disp_ests * final plot * fix return type * incorporate the review --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1992a2b commit a0a81cc

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

120 KB
Loading

pertpy/tools/_differential_gene_expression/_pydeseq2.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import os
22
import warnings
33

4+
import matplotlib.pyplot as plt
45
import numpy as np
56
import pandas as pd
67
from anndata import AnnData
8+
from matplotlib.lines import Line2D
9+
from matplotlib.pyplot import Figure
710
from numpy import ndarray
811
from pydeseq2.dds import DeseqDataSet
912
from pydeseq2.default_inference import DefaultInference
1013
from pydeseq2.ds import DeseqStats
1114
from scipy.sparse import issparse
1215

16+
from pertpy._doc import _doc_params, doc_common_plot_args
17+
1318
from ._base import LinearModelBase
1419
from ._checks import check_is_integer_matrix
1520

@@ -66,6 +71,162 @@ def fit(self, **kwargs) -> pd.DataFrame:
6671
dds.deseq2()
6772
self.dds = dds
6873

74+
@_doc_params(common_plot_args=doc_common_plot_args)
75+
def plot_disp_ests( # pragma: no cover # noqa: D417
76+
self,
77+
*,
78+
ymin: float | None = None,
79+
cv: bool = False,
80+
gene_col: str = "black",
81+
fit_col: str = "red",
82+
final_col: str = "dodgerblue",
83+
legend: bool = True,
84+
xlabel: str | None = None,
85+
ylabel: str | None = None,
86+
log: str = "xy",
87+
point_size: float = 0.45,
88+
return_fig: bool = False,
89+
**kwargs,
90+
) -> Figure | None:
91+
"""Plots per-gene dispersion estimates together with the fitted mean–dispersion relationship.
92+
93+
Args:
94+
ymin: Lower bound for plotted values. Points below this threshold are drawn at ymin using triangle markers.
95+
cv: If True, plot the square root of dispersion (coefficient of variation) instead of dispersion.
96+
gene_col: Color for gene-wise dispersion estimates.
97+
fit_col: Color for fitted dispersion trend.
98+
final_col: Color for final dispersion estimates used for testing.
99+
legend: Whether to draw a legend.
100+
xlabel: Label for the x-axis (default: "mean of normalized counts").
101+
ylabel: Label for the y-axis (default: "dispersion" or "coefficient of variation").
102+
log: Axis scaling. "x", "y", or "xy" for log scaling.
103+
point_size: Scaling factor for point sizes.
104+
{common_plot_args}
105+
**kwargs: Additional arguments for ax.scatter.
106+
107+
Returns:
108+
If `return_fig` is `True`, returns the figure, otherwise `None`.
109+
110+
Examples:
111+
>>> import pertpy as pt
112+
>>> import decoupler as dc
113+
>>> adata = pt.dt.zhang_2021()
114+
>>> adata = adata[adata.obs["Origin"] == "t", :].copy()
115+
>>> adata.layers["counts"] = adata.X.copy()
116+
>>> pdata = dc.pp.pseudobulk(adata, sample_col="Patient", groups_col="Cluster", layer="counts", mode="sum")
117+
>>> dc.pp.filter_samples(pdata, inplace=True)
118+
>>> pds2 = pt.tl.PyDESeq2(pdata, design="~Efficacy+Treatment")
119+
>>> pds2.fit()
120+
>>> pds2.plot_disp_ests(point_size=0.1)
121+
122+
Preview:
123+
.. image:: /_static/docstring_previews/de_disp_ests.png
124+
"""
125+
if not hasattr(self, "dds"):
126+
raise ValueError("Model not fitted yet. Call .fit() first.")
127+
128+
dds = self.dds
129+
130+
if xlabel is None:
131+
xlabel = "mean of normalized counts"
132+
if ylabel is None:
133+
ylabel = "coefficient of variation" if cv else "dispersion"
134+
135+
px = np.asarray(dds.var["_normed_means"])
136+
sel = px > 0
137+
px = px[sel]
138+
139+
py = np.asarray(dds.var["genewise_dispersions"])[sel]
140+
if cv:
141+
py = np.sqrt(py)
142+
143+
if ymin is None:
144+
positive = py[(py > 0) & np.isfinite(py)]
145+
ymin = 10 ** np.floor(np.log10(np.min(positive)) - 0.1)
146+
147+
py_plot = np.maximum(py, ymin)
148+
149+
fig, ax = plt.subplots(dpi=300)
150+
151+
below = py < ymin
152+
above = ~below
153+
154+
if above.any():
155+
ax.scatter(
156+
px[above],
157+
py_plot[above],
158+
facecolor=gene_col,
159+
edgecolors="none",
160+
s=point_size * 20,
161+
marker="o",
162+
**kwargs,
163+
)
164+
165+
if below.any():
166+
ax.scatter(
167+
px[below],
168+
py_plot[below],
169+
facecolor=gene_col,
170+
edgecolors="none",
171+
s=point_size * 20,
172+
marker="v",
173+
**kwargs,
174+
)
175+
176+
outliers = np.asarray(
177+
dds.var.get(
178+
"_outlier_genes",
179+
pd.Series(False, index=dds.var_names),
180+
)
181+
)[sel]
182+
183+
final_disp = np.asarray(dds.var["dispersions"])[sel]
184+
final_y = np.sqrt(final_disp) if cv else final_disp
185+
186+
ax.scatter(
187+
px,
188+
final_y,
189+
s=point_size * (20 + 20 * outliers.astype(int)),
190+
facecolor=np.where(outliers, "none", final_col),
191+
edgecolors=np.where(outliers, final_col, "none"),
192+
)
193+
194+
fitted_disp = np.asarray(dds.var["fitted_dispersions"])[sel]
195+
fitted_y = np.sqrt(fitted_disp) if cv else fitted_disp
196+
197+
ax.scatter(
198+
px,
199+
fitted_y,
200+
facecolor=fit_col,
201+
edgecolors="none",
202+
marker="o",
203+
s=point_size * 20,
204+
)
205+
206+
if "x" in log:
207+
ax.set_xscale("log")
208+
if "y" in log:
209+
ax.set_yscale("log")
210+
211+
ax.set_xlabel(xlabel)
212+
ax.set_ylabel(ylabel)
213+
214+
if legend:
215+
handles = [
216+
Line2D([0], [0], marker="o", linestyle="", color=gene_col, label="gene-est"),
217+
Line2D([0], [0], marker="o", linestyle="", color=fit_col, label="fitted"),
218+
Line2D([0], [0], marker="o", linestyle="", color=final_col, label="final"),
219+
]
220+
ax.legend(handles=handles, loc="lower right", frameon=True)
221+
222+
plt.tight_layout(pad=2.0)
223+
224+
if return_fig:
225+
return plt.gcf()
226+
227+
plt.show()
228+
return None
229+
69230
def _test_single_contrast(self, contrast, alpha=0.05, *, lfc_shrink=None, **kwargs) -> pd.DataFrame:
70231
"""Conduct a specific test and returns a Pandas DataFrame.
71232

0 commit comments

Comments
 (0)