scdiagnostics 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {scdiagnostics-0.1.2/src/scdiagnostics.egg-info → scdiagnostics-0.2.0}/PKG-INFO +6 -6
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/pyproject.toml +6 -6
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/__init__.py +6 -6
- scdiagnostics-0.2.0/src/scdiagnostics/plot_fit.py +283 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0/src/scdiagnostics.egg-info}/PKG-INFO +6 -6
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics.egg-info/SOURCES.txt +1 -1
- scdiagnostics-0.2.0/src/scdiagnostics.egg-info/requires.txt +10 -0
- scdiagnostics-0.1.2/src/scdiagnostics/pseudotime.py +0 -151
- scdiagnostics-0.1.2/src/scdiagnostics.egg-info/requires.txt +0 -10
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.github/scripts/bump_version.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.github/workflows/release.yml +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.gitignore +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/README.md +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/assets/atac_example.png +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/docs/quickstart.ipynb +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/setup.cfg +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/bivariate.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/data.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/dimred.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/marginal.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/spatial.py +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics.egg-info/dependency_links.txt +0 -0
- {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scdiagnostics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Author-email: Kris Sankaran <ksankaran@wisc.edu>
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: altair
|
|
7
|
-
Requires-Dist: pyarrow
|
|
8
|
-
Requires-Dist: scanpy
|
|
9
|
-
Requires-Dist: scipy
|
|
10
|
-
Requires-Dist: vegafusion
|
|
6
|
+
Requires-Dist: altair>=6.0.0
|
|
7
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
8
|
+
Requires-Dist: scanpy>=1.12
|
|
9
|
+
Requires-Dist: scipy>=1.15.2
|
|
10
|
+
Requires-Dist: vegafusion>=2.0.3
|
|
11
11
|
Requires-Dist: vl-convert-python>=1.8.0
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -7,15 +7,15 @@ name = "scdiagnostics"
|
|
|
7
7
|
authors = [
|
|
8
8
|
{ name="Kris Sankaran", email="ksankaran@wisc.edu" },
|
|
9
9
|
]
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.2.0"
|
|
11
11
|
description = ""
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"altair
|
|
15
|
-
"pyarrow
|
|
16
|
-
"scanpy
|
|
17
|
-
"scipy
|
|
18
|
-
"vegafusion
|
|
14
|
+
"altair>=6.0.0",
|
|
15
|
+
"pyarrow>=23.0.1",
|
|
16
|
+
"scanpy>=1.12",
|
|
17
|
+
"scipy>=1.15.2",
|
|
18
|
+
"vegafusion>=2.0.3",
|
|
19
19
|
"vl-convert-python>=1.8.0"
|
|
20
20
|
]
|
|
21
21
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from .pseudotime import plot_fitted_curve
|
|
2
1
|
from .bivariate import compare_correlation, compare_gene_pairs
|
|
3
2
|
from .dimred import plot_umap, plot_pca, compare_pca, compare_umap, overlay_expression
|
|
4
3
|
from .marginal import (
|
|
@@ -13,8 +12,9 @@ from .marginal import (
|
|
|
13
12
|
compare_histogram2,
|
|
14
13
|
gene_summary,
|
|
15
14
|
)
|
|
16
|
-
from .spatial import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
from .spatial import plot_dispersion_surface, plot_mean_surface, plot_spatial
|
|
16
|
+
from .plot_fit import (
|
|
17
|
+
plot_gene_correlations,
|
|
18
|
+
plot_fit_by_covariate,
|
|
19
|
+
plot_sampled_expression_by_covariate,
|
|
20
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Diagnostic plots for assessing fitted single-cell generative models.
|
|
2
|
+
|
|
3
|
+
The plotting functions in this module compare observed expression with predictions
|
|
4
|
+
or samples from a fitted model.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import altair as alt
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import scipy.sparse as sp
|
|
11
|
+
|
|
12
|
+
alt.data_transformers.enable("vegafusion")
|
|
13
|
+
alt.renderers.enable("png")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plot_gene_correlations(sim, num_genes=5):
|
|
17
|
+
"""Plot pairwise expression relationships in a simulated sample.
|
|
18
|
+
|
|
19
|
+
Expression is transformed with ``log1p`` before plotting. The first
|
|
20
|
+
``num_genes`` genes in the sampled data are shown as a scatterplot matrix.
|
|
21
|
+
"""
|
|
22
|
+
sampled = sim.sample()
|
|
23
|
+
expression = (
|
|
24
|
+
sampled.X.toarray() if sp.issparse(sampled.X) else np.asarray(sampled.X)
|
|
25
|
+
)
|
|
26
|
+
expression = pd.DataFrame(
|
|
27
|
+
np.log1p(expression),
|
|
28
|
+
columns=sampled.var_names,
|
|
29
|
+
index=sampled.obs.index,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
sampled_subset = pd.concat([expression.iloc[:, :num_genes], sampled.obs], axis=1)
|
|
33
|
+
genes = list(sampled.var_names[:num_genes])
|
|
34
|
+
return (
|
|
35
|
+
alt.Chart(sampled_subset)
|
|
36
|
+
.mark_circle(opacity=1, size=1)
|
|
37
|
+
.encode(
|
|
38
|
+
alt.X(alt.repeat("column"), type="quantitative"),
|
|
39
|
+
alt.Y(alt.repeat("row"), type="quantitative"),
|
|
40
|
+
)
|
|
41
|
+
.properties(width=40, height=40)
|
|
42
|
+
.repeat(row=genes, column=genes)
|
|
43
|
+
.configure_axis(labelFontSize=9, titleFontSize=9)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_DISTRIBUTIONS = {"negbin", "poisson", "bernoulli", "gaussian"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_distribution(distribution):
|
|
51
|
+
distribution = distribution.lower()
|
|
52
|
+
if distribution not in _DISTRIBUTIONS:
|
|
53
|
+
options = ", ".join(sorted(_DISTRIBUTIONS))
|
|
54
|
+
raise ValueError(f"distribution must be one of: {options}")
|
|
55
|
+
return distribution
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _covariate_info(sce, covariate):
|
|
59
|
+
if covariate not in sce.obs.columns:
|
|
60
|
+
raise KeyError(f"{covariate!r} is not a column in sce.obs")
|
|
61
|
+
|
|
62
|
+
values = sce.obs[covariate]
|
|
63
|
+
if values.isna().any():
|
|
64
|
+
raise ValueError(f"sce.obs[{covariate!r}] contains missing values")
|
|
65
|
+
numeric = pd.api.types.is_numeric_dtype(values) and not pd.api.types.is_bool_dtype(
|
|
66
|
+
values
|
|
67
|
+
)
|
|
68
|
+
if numeric:
|
|
69
|
+
return values, True, None
|
|
70
|
+
|
|
71
|
+
values = values.astype(str)
|
|
72
|
+
return values, False, list(dict.fromkeys(values))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _standard_deviation(predictions, gene_ix, distribution):
|
|
76
|
+
"""Return the fitted standard deviation."""
|
|
77
|
+
mean = np.asarray(predictions["mean"][:, gene_ix])
|
|
78
|
+
if distribution == "negbin":
|
|
79
|
+
dispersion = np.asarray(predictions["dispersion"][:, gene_ix])
|
|
80
|
+
return np.sqrt(mean + mean**2 / dispersion)
|
|
81
|
+
if distribution == "poisson":
|
|
82
|
+
return np.sqrt(mean)
|
|
83
|
+
if distribution == "gaussian":
|
|
84
|
+
return np.asarray(predictions["sdev"][:, gene_ix])
|
|
85
|
+
return np.sqrt(np.clip(mean * (1 - mean), 0, None))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _numeric_fit_chart(observed, mean, sdev, covariate, distribution):
|
|
89
|
+
x = alt.X(field=covariate, type="quantitative", title=covariate)
|
|
90
|
+
y = alt.Y("expression:Q", title="Expression")
|
|
91
|
+
points = alt.Chart(observed).mark_circle(size=20, opacity=0.35).encode(x=x, y=y)
|
|
92
|
+
fitted = pd.DataFrame({covariate: observed[covariate], "mean": mean})
|
|
93
|
+
|
|
94
|
+
if distribution == "bernoulli":
|
|
95
|
+
fitted = fitted.groupby(covariate, as_index=False, sort=True).mean()
|
|
96
|
+
return points + alt.Chart(fitted).mark_circle(size=20, color="red").encode(
|
|
97
|
+
x=x, y=alt.Y("mean:Q", title="Expression")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
fitted["second_moment"] = sdev**2 + fitted["mean"] ** 2
|
|
101
|
+
fitted = fitted.groupby(covariate, as_index=False, sort=True).mean()
|
|
102
|
+
fitted["sdev"] = np.sqrt(
|
|
103
|
+
np.clip(fitted["second_moment"] - fitted["mean"] ** 2, 0, None)
|
|
104
|
+
)
|
|
105
|
+
fitted["lower"] = fitted["mean"] - fitted["sdev"]
|
|
106
|
+
fitted["upper"] = fitted["mean"] + fitted["sdev"]
|
|
107
|
+
band = alt.Chart(fitted).mark_area(opacity=0.3, color="orange").encode(
|
|
108
|
+
x=x, y=alt.Y("lower:Q", title="Expression"), y2="upper:Q"
|
|
109
|
+
)
|
|
110
|
+
line = alt.Chart(fitted).mark_line(color="red").encode(
|
|
111
|
+
x=x, y=alt.Y("mean:Q", title="Expression")
|
|
112
|
+
)
|
|
113
|
+
return band + line + points
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _categorical_fit_chart(observed, mean, sdev, covariate, category_order):
|
|
117
|
+
observed = observed.copy()
|
|
118
|
+
observed["jitter"] = np.random.default_rng(0).uniform(-0.5, 0.5, len(observed))
|
|
119
|
+
fitted = pd.DataFrame(
|
|
120
|
+
{covariate: observed[covariate], "mean": mean, "sdev": sdev}
|
|
121
|
+
)
|
|
122
|
+
fitted["second_moment"] = fitted["sdev"] ** 2 + fitted["mean"] ** 2
|
|
123
|
+
fitted = fitted.groupby(covariate, sort=False, observed=True, as_index=False).mean()
|
|
124
|
+
fitted["sdev"] = np.sqrt(
|
|
125
|
+
np.clip(fitted["second_moment"] - fitted["mean"] ** 2, 0, None)
|
|
126
|
+
)
|
|
127
|
+
fitted["lower"] = fitted["mean"] - fitted["sdev"]
|
|
128
|
+
fitted["upper"] = fitted["mean"] + fitted["sdev"]
|
|
129
|
+
fitted["jitter"] = 0.0
|
|
130
|
+
x = alt.X(
|
|
131
|
+
field=covariate, type="nominal", title=covariate, sort=category_order,
|
|
132
|
+
)
|
|
133
|
+
jitter = alt.XOffset(
|
|
134
|
+
"jitter:Q", scale=alt.Scale(domain=[-0.5, 0.5])
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
error_bars = alt.Chart(fitted).mark_rule(color="red").encode(
|
|
138
|
+
x=x,
|
|
139
|
+
xOffset=jitter,
|
|
140
|
+
y=alt.Y("lower:Q", title="Expression"),
|
|
141
|
+
y2="upper:Q",
|
|
142
|
+
)
|
|
143
|
+
lower_caps = alt.Chart(fitted).mark_tick(
|
|
144
|
+
color="red", orient="horizontal", size=24
|
|
145
|
+
).encode(x=x, xOffset=jitter, y="lower:Q")
|
|
146
|
+
upper_caps = alt.Chart(fitted).mark_tick(
|
|
147
|
+
color="red", orient="horizontal", size=24
|
|
148
|
+
).encode(x=x, xOffset=jitter, y="upper:Q")
|
|
149
|
+
fitted_means = alt.Chart(fitted).mark_point(color="red", filled=True, size=55).encode(
|
|
150
|
+
x=x, xOffset=jitter, y=alt.Y("mean:Q", title="Expression")
|
|
151
|
+
)
|
|
152
|
+
points = alt.Chart(observed).mark_circle(size=24, opacity=0.35).encode(
|
|
153
|
+
x=x,
|
|
154
|
+
xOffset=jitter,
|
|
155
|
+
y=alt.Y("expression:Q", title="Expression"),
|
|
156
|
+
)
|
|
157
|
+
return points + error_bars + lower_caps + upper_caps + fitted_means
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def plot_fit_by_covariate(
|
|
161
|
+
sim,
|
|
162
|
+
sce,
|
|
163
|
+
distribution="negbin",
|
|
164
|
+
covariate="pseudotime",
|
|
165
|
+
num_genes=5,
|
|
166
|
+
):
|
|
167
|
+
"""Plot fitted gene expression against a numeric or categorical covariate.
|
|
168
|
+
|
|
169
|
+
Numeric covariates show observations and fitted values; negative-binomial,
|
|
170
|
+
Poisson, and Gaussian fits also show a one-standard-deviation band.
|
|
171
|
+
Categorical covariates show jittered observations with the model's
|
|
172
|
+
category-level mean and one-standard-deviation error bars.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
distribution : {"negbin", "poisson", "bernoulli", "gaussian"}
|
|
177
|
+
Distribution used by the fitted model.
|
|
178
|
+
covariate : str
|
|
179
|
+
Column in ``sce.obs``. Its dtype determines which plot is produced.
|
|
180
|
+
"""
|
|
181
|
+
distribution = _validate_distribution(distribution)
|
|
182
|
+
values, numeric, category_order = _covariate_info(sce, covariate)
|
|
183
|
+
|
|
184
|
+
expression = sce.X.toarray() if sp.issparse(sce.X) else np.asarray(sce.X)
|
|
185
|
+
predictions = sim.predict(sce.obs)
|
|
186
|
+
charts = []
|
|
187
|
+
for ix in range(min(num_genes, sce.n_vars)):
|
|
188
|
+
mean = np.asarray(predictions["mean"][:, ix])
|
|
189
|
+
observed = pd.DataFrame(
|
|
190
|
+
{covariate: values.to_numpy(), "expression": expression[:, ix]}
|
|
191
|
+
)
|
|
192
|
+
sdev = _standard_deviation(predictions, ix, distribution)
|
|
193
|
+
if numeric:
|
|
194
|
+
chart = _numeric_fit_chart(
|
|
195
|
+
observed, mean, sdev, covariate, distribution
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
chart = _categorical_fit_chart(
|
|
199
|
+
observed, mean, sdev, covariate, category_order
|
|
200
|
+
)
|
|
201
|
+
charts.append(
|
|
202
|
+
chart.properties(
|
|
203
|
+
title=f"Gene {ix}: {sce.var_names[ix]}", width=300, height=200
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return alt.vconcat(*charts)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def plot_sampled_expression_by_covariate(
|
|
211
|
+
sim,
|
|
212
|
+
sce,
|
|
213
|
+
covariate="pseudotime",
|
|
214
|
+
num_genes=5,
|
|
215
|
+
jitter=False,
|
|
216
|
+
):
|
|
217
|
+
"""Compare real and simulated expression by a numeric or categorical covariate.
|
|
218
|
+
|
|
219
|
+
A sample is drawn from the fitted model at ``sce.obs``. Numeric covariates
|
|
220
|
+
use an overlaid scatter plot. Categorical covariates use horizontally
|
|
221
|
+
jittered points within each category. Color distinguishes real and simulated
|
|
222
|
+
expression.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
covariate : str
|
|
227
|
+
Column in ``sce.obs``. Its dtype determines which plot is produced.
|
|
228
|
+
jitter : bool
|
|
229
|
+
Add small vertical jitter to expression values. Horizontal jitter is
|
|
230
|
+
always applied for categorical covariates.
|
|
231
|
+
"""
|
|
232
|
+
values, numeric, category_order = _covariate_info(sce, covariate)
|
|
233
|
+
|
|
234
|
+
real_X = sce.X.toarray() if sp.issparse(sce.X) else np.asarray(sce.X)
|
|
235
|
+
sampled = sim.sample(sce.obs)
|
|
236
|
+
sim_X = sampled.X.toarray() if sp.issparse(sampled.X) else np.asarray(sampled.X)
|
|
237
|
+
if real_X.shape != sim_X.shape:
|
|
238
|
+
raise ValueError("real and simulated expression matrices must have equal shapes")
|
|
239
|
+
|
|
240
|
+
rng = np.random.default_rng(0)
|
|
241
|
+
charts = []
|
|
242
|
+
for ix in range(min(num_genes, sce.n_vars)):
|
|
243
|
+
real_values = real_X[:, ix]
|
|
244
|
+
simulated_values = sim_X[:, ix]
|
|
245
|
+
if jitter:
|
|
246
|
+
real_values = real_values + rng.normal(0, 0.1, len(values))
|
|
247
|
+
simulated_values = simulated_values + rng.normal(0, 0.1, len(values))
|
|
248
|
+
|
|
249
|
+
combined = pd.DataFrame(
|
|
250
|
+
{
|
|
251
|
+
covariate: np.tile(values.to_numpy(), 2),
|
|
252
|
+
"expression": np.concatenate([real_values, simulated_values]),
|
|
253
|
+
"type": np.repeat(["real", "simulated"], len(values)),
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
x = alt.X(
|
|
257
|
+
field=covariate,
|
|
258
|
+
type="quantitative" if numeric else "nominal",
|
|
259
|
+
title=covariate,
|
|
260
|
+
sort=None if numeric else category_order,
|
|
261
|
+
)
|
|
262
|
+
if not numeric:
|
|
263
|
+
combined["x_jitter"] = rng.uniform(-0.5, 0.5, len(combined))
|
|
264
|
+
|
|
265
|
+
encoding = {
|
|
266
|
+
"x": x,
|
|
267
|
+
"y": alt.Y("expression:Q", title="Expression"),
|
|
268
|
+
"color": alt.Color("type:N", title=None),
|
|
269
|
+
}
|
|
270
|
+
if not numeric:
|
|
271
|
+
encoding["xOffset"] = alt.XOffset(
|
|
272
|
+
"x_jitter:Q", scale=alt.Scale(domain=[-0.5, 0.5])
|
|
273
|
+
)
|
|
274
|
+
charts.append(
|
|
275
|
+
alt.Chart(combined)
|
|
276
|
+
.mark_circle(size=24, opacity=0.35)
|
|
277
|
+
.encode(**encoding)
|
|
278
|
+
.properties(
|
|
279
|
+
title=f"Gene {ix}: {sce.var_names[ix]}", width=300, height=200
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return alt.vconcat(*charts)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scdiagnostics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Author-email: Kris Sankaran <ksankaran@wisc.edu>
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: altair
|
|
7
|
-
Requires-Dist: pyarrow
|
|
8
|
-
Requires-Dist: scanpy
|
|
9
|
-
Requires-Dist: scipy
|
|
10
|
-
Requires-Dist: vegafusion
|
|
6
|
+
Requires-Dist: altair>=6.0.0
|
|
7
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
8
|
+
Requires-Dist: scanpy>=1.12
|
|
9
|
+
Requires-Dist: scipy>=1.15.2
|
|
10
|
+
Requires-Dist: vegafusion>=2.0.3
|
|
11
11
|
Requires-Dist: vl-convert-python>=1.8.0
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -10,7 +10,7 @@ src/scdiagnostics/bivariate.py
|
|
|
10
10
|
src/scdiagnostics/data.py
|
|
11
11
|
src/scdiagnostics/dimred.py
|
|
12
12
|
src/scdiagnostics/marginal.py
|
|
13
|
-
src/scdiagnostics/
|
|
13
|
+
src/scdiagnostics/plot_fit.py
|
|
14
14
|
src/scdiagnostics/spatial.py
|
|
15
15
|
src/scdiagnostics.egg-info/PKG-INFO
|
|
16
16
|
src/scdiagnostics.egg-info/SOURCES.txt
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"""Plot observed expression and model fitted mean/sd along one-dimensional pseudotime."""
|
|
2
|
-
|
|
3
|
-
import altair as alt
|
|
4
|
-
import pandas as pd
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def plot_fitted_curve(
|
|
8
|
-
observed,
|
|
9
|
-
fitted,
|
|
10
|
-
*,
|
|
11
|
-
x,
|
|
12
|
-
y,
|
|
13
|
-
mean="mean",
|
|
14
|
-
group=None,
|
|
15
|
-
model=None,
|
|
16
|
-
lower=None,
|
|
17
|
-
upper=None,
|
|
18
|
-
n_bins=10,
|
|
19
|
-
transform=None,
|
|
20
|
-
y_title=None,
|
|
21
|
-
width=300,
|
|
22
|
-
height=220,
|
|
23
|
-
columns=2,
|
|
24
|
-
):
|
|
25
|
-
"""Plot observations and model fits against one-dimensional time or pseudotime.
|
|
26
|
-
|
|
27
|
-
Parameters
|
|
28
|
-
----------
|
|
29
|
-
observed, fitted : pandas.DataFrame
|
|
30
|
-
Two pandas DFs. Observed should have columns `x`, `y`, and optionally a
|
|
31
|
-
`group`. Fitted should have `x`, `mean`, optionally `group`, `model`,
|
|
32
|
-
and the intervals. Each `model` will get a curve.
|
|
33
|
-
x, y, mean : str
|
|
34
|
-
Column names for the feature to plot against, the response, and the
|
|
35
|
-
fitted mean.
|
|
36
|
-
group, model : str, optional
|
|
37
|
-
Column names to facet by (group) and to use for separate lines (model).
|
|
38
|
-
lower, upper : str, optional
|
|
39
|
-
Column names for the intervals.
|
|
40
|
-
n_bins : int or None
|
|
41
|
-
Equal-width bins used for averaging. Defaults to None.
|
|
42
|
-
transform : callable, optional
|
|
43
|
-
Transform applied after averaging within bins. E.g., np.log1p plots
|
|
44
|
-
log(1 + E[Y|x]).
|
|
45
|
-
y_title : str, optional
|
|
46
|
-
Axis label.
|
|
47
|
-
columns : int, default 2
|
|
48
|
-
Number of facet columns.
|
|
49
|
-
|
|
50
|
-
Returns
|
|
51
|
-
-------
|
|
52
|
-
altair.LayerChart or altair.FacetChart
|
|
53
|
-
Observations, fitted curves, and optional intervals and binned means.
|
|
54
|
-
"""
|
|
55
|
-
obs, fit = _prepare_tables(
|
|
56
|
-
observed, fitted, x=x, y=y, mean=mean, group=group, model=model,
|
|
57
|
-
lower=lower, upper=upper,
|
|
58
|
-
)
|
|
59
|
-
tables = [obs, fit]
|
|
60
|
-
if n_bins is not None:
|
|
61
|
-
tables.append(_bin_observations(obs, n_bins))
|
|
62
|
-
data = pd.concat(tables, ignore_index=True)
|
|
63
|
-
if transform is not None:
|
|
64
|
-
data = _transform_response(data, transform)
|
|
65
|
-
|
|
66
|
-
chart = _curve_layers(
|
|
67
|
-
data, x_title=x, y_title=y_title or y, model_title=model or "Model",
|
|
68
|
-
show_intervals=lower is not None, show_bins=n_bins is not None,
|
|
69
|
-
).properties(width=width, height=height)
|
|
70
|
-
if group is not None:
|
|
71
|
-
chart = chart.facet(facet=alt.Facet("_group:N", title=group), columns=columns)
|
|
72
|
-
chart = chart.resolve_scale(y="independent")
|
|
73
|
-
return chart
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _prepare_tables(observed, fitted, *, x, y, mean, group, model, lower, upper):
|
|
77
|
-
"""Copy input columns into the shared internal schema used by plot layers."""
|
|
78
|
-
obs = pd.DataFrame({
|
|
79
|
-
"_x": observed[x].to_numpy(),
|
|
80
|
-
"_y": observed[y].to_numpy(),
|
|
81
|
-
"_group": observed[group].to_numpy() if group is not None else "All",
|
|
82
|
-
"_kind": "observed",
|
|
83
|
-
})
|
|
84
|
-
fit = pd.DataFrame({
|
|
85
|
-
"_x": fitted[x].to_numpy(),
|
|
86
|
-
"_y": fitted[mean].to_numpy(),
|
|
87
|
-
"_group": fitted[group].to_numpy() if group is not None else "All",
|
|
88
|
-
"_model": fitted[model].to_numpy() if model is not None else "Fitted mean",
|
|
89
|
-
"_kind": "fitted",
|
|
90
|
-
})
|
|
91
|
-
if lower is not None:
|
|
92
|
-
fit["_lower"] = fitted[lower].to_numpy()
|
|
93
|
-
fit["_upper"] = fitted[upper].to_numpy()
|
|
94
|
-
return obs, fit
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _bin_observations(observed, n_bins):
|
|
98
|
-
"""Average time and response in equal-width bins within each group."""
|
|
99
|
-
tables = []
|
|
100
|
-
for label, part in observed.groupby("_group", sort=False, observed=True):
|
|
101
|
-
time_bins = pd.cut(part["_x"], n_bins)
|
|
102
|
-
means = part.groupby(time_bins, observed=True)[["_x", "_y"]].mean()
|
|
103
|
-
tables.append(means.assign(_group=label, _kind="binned"))
|
|
104
|
-
if not tables:
|
|
105
|
-
return observed.iloc[:0].copy()
|
|
106
|
-
return pd.concat(tables, ignore_index=True)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _transform_response(data, transform):
|
|
110
|
-
"""Transform response columns, leaving absent layer-specific values alone."""
|
|
111
|
-
data = data.copy()
|
|
112
|
-
for column in ("_y", "_lower", "_upper"):
|
|
113
|
-
data[column] = data[column].astype(float)
|
|
114
|
-
valid = data[column].notna()
|
|
115
|
-
data.loc[valid, column] = transform(data.loc[valid, column].to_numpy())
|
|
116
|
-
return data
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _curve_layers(data, *, x_title, y_title, model_title, show_intervals, show_bins):
|
|
120
|
-
"""Compose layers back-to-front using one shared dataset for faceting."""
|
|
121
|
-
base = alt.Chart(data).encode(
|
|
122
|
-
x=alt.X("_x:Q", title=x_title, scale=alt.Scale(zero=False)),
|
|
123
|
-
y=alt.Y("_y:Q", title=y_title, scale=alt.Scale(zero=False)),
|
|
124
|
-
)
|
|
125
|
-
fitted = base.transform_filter(alt.datum._kind == "fitted")
|
|
126
|
-
color = alt.Color("_model:N", title=model_title)
|
|
127
|
-
observations = base.transform_filter(alt.datum._kind == "observed").mark_circle(
|
|
128
|
-
color="black", opacity=0.25, size=15,
|
|
129
|
-
)
|
|
130
|
-
curves = fitted.mark_line().encode(color=color, order="_x:Q")
|
|
131
|
-
|
|
132
|
-
layers = []
|
|
133
|
-
if show_intervals:
|
|
134
|
-
bands = fitted.mark_area(opacity=0.12).encode(
|
|
135
|
-
y=alt.Y("_lower:Q", title=y_title, stack=None, scale=alt.Scale(zero=False)),
|
|
136
|
-
y2="_upper:Q",
|
|
137
|
-
color=color,
|
|
138
|
-
)
|
|
139
|
-
layers.append(bands)
|
|
140
|
-
layers.extend([observations, curves])
|
|
141
|
-
if show_bins:
|
|
142
|
-
binned_means = (
|
|
143
|
-
base.transform_filter(alt.datum._kind == "binned")
|
|
144
|
-
.mark_point(shape="diamond", filled=True, color="#6E9CB6", size=55)
|
|
145
|
-
.encode(tooltip=[
|
|
146
|
-
alt.Tooltip("_x:Q", title=x_title),
|
|
147
|
-
alt.Tooltip("_y:Q", title="Observed bin mean"),
|
|
148
|
-
])
|
|
149
|
-
)
|
|
150
|
-
layers.append(binned_means)
|
|
151
|
-
return alt.layer(*layers)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|