diffsvg 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffsvg/__init__.py +3 -0
- diffsvg/pl/__init__.py +5 -0
- diffsvg/pl/plot_clusters.py +216 -0
- diffsvg/pl/plot_results.py +484 -0
- diffsvg/pl/plot_stability.py +184 -0
- diffsvg/pp/__init__.py +10 -0
- diffsvg/pp/utils.py +620 -0
- diffsvg/py.typed +0 -0
- diffsvg/tl/__init__.py +24 -0
- diffsvg/tl/_exact.py +332 -0
- diffsvg/tl/clustering.py +154 -0
- diffsvg/tl/model.py +1857 -0
- diffsvg/tl/stability.py +335 -0
- diffsvg-0.1.0.dist-info/METADATA +132 -0
- diffsvg-0.1.0.dist-info/RECORD +17 -0
- diffsvg-0.1.0.dist-info/WHEEL +4 -0
- diffsvg-0.1.0.dist-info/licenses/LICENSE +29 -0
diffsvg/__init__.py
ADDED
diffsvg/pl/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from .plot_clusters import plot_cluster_curves
|
|
2
|
+
from .plot_results import plot_prediction_curve, plot_pvalue_histogram, plot_volcano
|
|
3
|
+
from .plot_stability import plot_stability
|
|
4
|
+
|
|
5
|
+
__all__ = ["plot_cluster_curves", "plot_prediction_curve", "plot_pvalue_histogram", "plot_stability", "plot_volcano"]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Plots for the output of `diffsvg.tl.cluster_curves`."""
|
|
2
|
+
|
|
3
|
+
import colorcet
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from diffsvg.tl.model import _gene_profiles
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _display_tables(adata, curves, by, curves_key, profiles_key):
|
|
11
|
+
"""``{panel title suffix: genes x bins table}``, one entry per panel of a cluster."""
|
|
12
|
+
# `_gene_profiles` prefers a `fit_profiles` result, which is not the curve `by="average"` compared.
|
|
13
|
+
if curves == "combined" and by != "average":
|
|
14
|
+
return {"": _gene_profiles(adata, curves_key, profiles_key)[0]}
|
|
15
|
+
|
|
16
|
+
key0, key1 = f"{curves_key}_cond0_mean", f"{curves_key}_cond1_mean"
|
|
17
|
+
if key0 not in adata.varm:
|
|
18
|
+
raise KeyError(f"adata.varm[{key0!r}] not found - run `diffsvg.tl.fit_model` first, with `inplace=True`.")
|
|
19
|
+
cond0, cond1 = adata.varm[key0], adata.varm[key1]
|
|
20
|
+
labels = adata.uns.get(f"{curves_key}_condition_categories", {"0": "condition 0", "1": "condition 1"})
|
|
21
|
+
if curves == "combined":
|
|
22
|
+
return {"": (cond0 + cond1) / 2.0}
|
|
23
|
+
if curves == "conditions":
|
|
24
|
+
return {labels["0"]: cond0, labels["1"]: cond1}
|
|
25
|
+
if curves == "difference":
|
|
26
|
+
return {f"{labels['1']} - {labels['0']}": cond1 - cond0}
|
|
27
|
+
if curves == labels["0"]:
|
|
28
|
+
return {curves: cond0}
|
|
29
|
+
if curves == labels["1"]:
|
|
30
|
+
return {curves: cond1}
|
|
31
|
+
available = ["combined", "conditions", "difference", labels["0"], labels["1"]]
|
|
32
|
+
raise ValueError(f"Unknown curves {curves!r}. Available: {available}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _standardise(values, standardize):
|
|
36
|
+
"""Per gene over all of `values` (genes x points, panels side by side), so a gap between panels survives."""
|
|
37
|
+
if standardize == "none":
|
|
38
|
+
return values
|
|
39
|
+
values = values - values.mean(axis=1, keepdims=True)
|
|
40
|
+
if standardize == "zscore":
|
|
41
|
+
sd = values.std(axis=1, keepdims=True)
|
|
42
|
+
values = values / np.where(sd > 0, sd, 1.0)
|
|
43
|
+
return values
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _legend_cols(n_genes):
|
|
47
|
+
"""Legend columns for a cluster of `n_genes`, about 24 names to a column (a panel's height at the default size)."""
|
|
48
|
+
return (n_genes + 23) // 24
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#: Inches around a panel: the axis labels on its left, the title above and the x-axis label below it.
|
|
52
|
+
_PANEL_MARGINS = {"left": 0.65, "right": 0.15, "top": 0.4, "bottom": 0.55}
|
|
53
|
+
#: Inches a legend column takes next to a panel.
|
|
54
|
+
_LEGEND_COLUMN_WIDTH = 1.0
|
|
55
|
+
|
|
56
|
+
_Y_LABELS = {"none": "smoothed expression", "center": "centred expression", "zscore": "z-scored expression"}
|
|
57
|
+
_DEFAULT_CURVES = {"interaction": "difference", "average": "combined", "profile": "combined"}
|
|
58
|
+
_GENE_COLOURS = colorcet.b_glasbey_category10
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def plot_cluster_curves(
|
|
62
|
+
adata,
|
|
63
|
+
cluster_key="dsvg_cluster_average",
|
|
64
|
+
curves=None,
|
|
65
|
+
clusters=None,
|
|
66
|
+
standardize=None,
|
|
67
|
+
curves_key="dsvg_curves",
|
|
68
|
+
profiles_key="dsvg_profiles",
|
|
69
|
+
ncols=3,
|
|
70
|
+
figsize=(4, 3.2),
|
|
71
|
+
show_legend=True,
|
|
72
|
+
dpi=300,
|
|
73
|
+
save_path=None,
|
|
74
|
+
):
|
|
75
|
+
"""One curve plot per cluster, with the fitted curve of each gene overlaid in its own colour.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
adata
|
|
80
|
+
Annotated data matrix that `diffsvg.tl.cluster_curves` wrote into.
|
|
81
|
+
cluster_key
|
|
82
|
+
The clustering, i.e. ``key_added`` of `diffsvg.tl.cluster_curves`.
|
|
83
|
+
curves
|
|
84
|
+
Which curves to draw:
|
|
85
|
+
|
|
86
|
+
- ``"combined"``: one curve per gene, the mean of the two condition
|
|
87
|
+
curves. For ``by="profile"``, and for labels that did not come from
|
|
88
|
+
`diffsvg.tl.cluster_curves`, the `diffsvg.tl.fit_profiles` fit if
|
|
89
|
+
there is one.
|
|
90
|
+
- ``"conditions"``: two panels per cluster, one per condition, in a
|
|
91
|
+
shared y-range and with the same gene colours.
|
|
92
|
+
- ``"difference"``: the difference curve ``f1 - f0``.
|
|
93
|
+
- A condition label, e.g. ``"SHH"``: that condition's curve only, one
|
|
94
|
+
panel per cluster.
|
|
95
|
+
|
|
96
|
+
Defaults to the curves the clustering compared: ``"difference"`` for
|
|
97
|
+
``by="interaction"``, otherwise ``"combined"``.
|
|
98
|
+
clusters
|
|
99
|
+
Clusters to draw, by default all of them.
|
|
100
|
+
standardize
|
|
101
|
+
Per gene: ``"none"`` (expression as fitted), ``"center"`` (mean
|
|
102
|
+
removed, so levels do not separate the curves; the clustering does not
|
|
103
|
+
see levels either) or ``"zscore"`` (also unit variance). With two
|
|
104
|
+
panels the mean and variance are taken over both, so the gap between
|
|
105
|
+
the conditions stays visible. Defaults to ``"zscore"`` if the
|
|
106
|
+
clustering scaled the genes, otherwise ``"center"``.
|
|
107
|
+
curves_key, profiles_key
|
|
108
|
+
Prefixes passed to `diffsvg.tl.fit_model` / `diffsvg.tl.fit_profiles`.
|
|
109
|
+
ncols
|
|
110
|
+
Panels per row. With ``curves="conditions"`` a row always holds one
|
|
111
|
+
cluster, so this is ignored.
|
|
112
|
+
figsize
|
|
113
|
+
``(width, height)`` of a single panel in inches.
|
|
114
|
+
show_legend
|
|
115
|
+
Name the genes in a legend, per cluster. Colours repeat only between
|
|
116
|
+
clusters, not within one.
|
|
117
|
+
dpi
|
|
118
|
+
Resolution used when saving to `save_path`.
|
|
119
|
+
save_path
|
|
120
|
+
If given, save the figure here (``bbox_inches="tight"``).
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
An array of `matplotlib.axes.Axes`, row by row: one panel per cluster, or
|
|
125
|
+
two with ``curves="conditions"``. Each gene is a line labelled with its name.
|
|
126
|
+
|
|
127
|
+
Examples
|
|
128
|
+
--------
|
|
129
|
+
>>> dvg.tl.cluster_curves(adata, by="interaction", n_clusters=4)
|
|
130
|
+
>>> dvg.pl.plot_cluster_curves(adata, "dsvg_cluster_interaction", curves="conditions")
|
|
131
|
+
"""
|
|
132
|
+
if cluster_key not in adata.var:
|
|
133
|
+
raise KeyError(f"adata.var[{cluster_key!r}] not found - run `diffsvg.tl.cluster_curves` first.")
|
|
134
|
+
params = adata.uns.get(cluster_key, {})
|
|
135
|
+
if curves is None:
|
|
136
|
+
curves = _DEFAULT_CURVES.get(params.get("by"), "combined")
|
|
137
|
+
if standardize is None:
|
|
138
|
+
standardize = "zscore" if params.get("scale", False) else "center"
|
|
139
|
+
if standardize not in _Y_LABELS:
|
|
140
|
+
raise ValueError(f"Unknown standardize {standardize!r}. Available: {sorted(_Y_LABELS)}")
|
|
141
|
+
|
|
142
|
+
tables = _display_tables(adata, curves, params.get("by"), curves_key, profiles_key)
|
|
143
|
+
assignment = adata.var[cluster_key].dropna()
|
|
144
|
+
if clusters is None:
|
|
145
|
+
clusters = list(assignment.cat.categories)
|
|
146
|
+
else:
|
|
147
|
+
clusters = [str(c) for c in ([clusters] if isinstance(clusters, str | int) else clusters)]
|
|
148
|
+
unknown = [c for c in clusters if c not in assignment.cat.categories]
|
|
149
|
+
if unknown:
|
|
150
|
+
raise KeyError(f"clusters not found in adata.var[{cluster_key!r}]: {unknown}")
|
|
151
|
+
|
|
152
|
+
width, height = figsize
|
|
153
|
+
n_panels = len(clusters) * len(tables)
|
|
154
|
+
ncols = len(tables) if len(tables) > 1 else max(1, min(ncols, n_panels))
|
|
155
|
+
nrows = (n_panels + ncols - 1) // ncols
|
|
156
|
+
# The legends sit next to their panels: the gap holds the widest one and the figure grows by it, so
|
|
157
|
+
# panels keep the size asked for. With two panels per cluster the legend is right of the row, not in its gap.
|
|
158
|
+
largest = max((assignment == c).sum() for c in clusters)
|
|
159
|
+
legend_width = _LEGEND_COLUMN_WIDTH * _legend_cols(largest) if show_legend else 0.0
|
|
160
|
+
gap_x = _PANEL_MARGINS["left"] + _PANEL_MARGINS["right"] + (legend_width if len(tables) == 1 else 0.0)
|
|
161
|
+
gap_y = _PANEL_MARGINS["top"] + _PANEL_MARGINS["bottom"]
|
|
162
|
+
fig_width = ncols * width + (ncols - 1) * gap_x + _PANEL_MARGINS["left"] + _PANEL_MARGINS["right"] + legend_width
|
|
163
|
+
fig_height = nrows * height + (nrows - 1) * gap_y + gap_y
|
|
164
|
+
fig, axes = plt.subplots(
|
|
165
|
+
nrows,
|
|
166
|
+
ncols,
|
|
167
|
+
figsize=(fig_width, fig_height),
|
|
168
|
+
squeeze=False,
|
|
169
|
+
gridspec_kw={
|
|
170
|
+
"left": _PANEL_MARGINS["left"] / fig_width,
|
|
171
|
+
"right": 1.0 - (_PANEL_MARGINS["right"] + legend_width) / fig_width,
|
|
172
|
+
"bottom": _PANEL_MARGINS["bottom"] / fig_height,
|
|
173
|
+
"top": 1.0 - _PANEL_MARGINS["top"] / fig_height,
|
|
174
|
+
"wspace": gap_x / width,
|
|
175
|
+
"hspace": gap_y / height,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
axes = axes.ravel()
|
|
179
|
+
for spare in axes[n_panels:]:
|
|
180
|
+
spare.set_visible(False)
|
|
181
|
+
|
|
182
|
+
x = next(iter(tables.values())).columns.to_numpy(dtype=float)
|
|
183
|
+
panels = iter(axes)
|
|
184
|
+
for cluster in clusters:
|
|
185
|
+
genes = list(assignment.index[assignment == cluster])
|
|
186
|
+
joined = _standardise(np.hstack([t.loc[genes].to_numpy(dtype=float) for t in tables.values()]), standardize)
|
|
187
|
+
lines = np.split(joined, len(tables), axis=1)
|
|
188
|
+
# A cluster's panels share a y-range, so the conditions can be compared; clusters keep their own.
|
|
189
|
+
margin = 0.05 * (np.nanmax(joined) - np.nanmin(joined)) or 1.0
|
|
190
|
+
ylim = (float(np.nanmin(joined) - margin), float(np.nanmax(joined) + margin))
|
|
191
|
+
for i, (suffix, values) in enumerate(zip(tables, lines, strict=True)):
|
|
192
|
+
ax = next(panels)
|
|
193
|
+
for g, gene in enumerate(genes):
|
|
194
|
+
ax.plot(x, values[g], color=_GENE_COLOURS[g % len(_GENE_COLOURS)], label=gene, lw=1.2)
|
|
195
|
+
ax.set_ylim(ylim)
|
|
196
|
+
ax.spines["top"].set_visible(False)
|
|
197
|
+
ax.spines["right"].set_visible(False)
|
|
198
|
+
ax.set_xlabel("Normalized distance")
|
|
199
|
+
ax.set_ylabel(_Y_LABELS[standardize])
|
|
200
|
+
ax.set_title(f"Cluster {cluster} (n={len(genes)})" + (f", {suffix}" if suffix else ""), fontsize=11)
|
|
201
|
+
# With two panels per cluster one legend is enough; it goes next to the second.
|
|
202
|
+
if show_legend and i == len(tables) - 1:
|
|
203
|
+
ax.legend(
|
|
204
|
+
loc="upper left",
|
|
205
|
+
bbox_to_anchor=(1.01, 1.0),
|
|
206
|
+
ncols=_legend_cols(len(genes)),
|
|
207
|
+
frameon=False,
|
|
208
|
+
fontsize=7,
|
|
209
|
+
borderaxespad=0.0,
|
|
210
|
+
handlelength=1.2,
|
|
211
|
+
columnspacing=0.8,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if save_path is not None:
|
|
215
|
+
fig.savefig(save_path, dpi=dpi, bbox_inches="tight")
|
|
216
|
+
return axes[:n_panels]
|