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 ADDED
@@ -0,0 +1,3 @@
1
+ from . import pl, pp, tl
2
+
3
+ __all__ = ["pl", "pp", "tl"]
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]