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.
Files changed (23) hide show
  1. {scdiagnostics-0.1.2/src/scdiagnostics.egg-info → scdiagnostics-0.2.0}/PKG-INFO +6 -6
  2. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/pyproject.toml +6 -6
  3. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/__init__.py +6 -6
  4. scdiagnostics-0.2.0/src/scdiagnostics/plot_fit.py +283 -0
  5. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0/src/scdiagnostics.egg-info}/PKG-INFO +6 -6
  6. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics.egg-info/SOURCES.txt +1 -1
  7. scdiagnostics-0.2.0/src/scdiagnostics.egg-info/requires.txt +10 -0
  8. scdiagnostics-0.1.2/src/scdiagnostics/pseudotime.py +0 -151
  9. scdiagnostics-0.1.2/src/scdiagnostics.egg-info/requires.txt +0 -10
  10. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.github/scripts/bump_version.py +0 -0
  11. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.github/workflows/release.yml +0 -0
  12. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/.gitignore +0 -0
  13. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/README.md +0 -0
  14. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/assets/atac_example.png +0 -0
  15. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/docs/quickstart.ipynb +0 -0
  16. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/setup.cfg +0 -0
  17. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/bivariate.py +0 -0
  18. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/data.py +0 -0
  19. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/dimred.py +0 -0
  20. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/marginal.py +0 -0
  21. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics/spatial.py +0 -0
  22. {scdiagnostics-0.1.2 → scdiagnostics-0.2.0}/src/scdiagnostics.egg-info/dependency_links.txt +0 -0
  23. {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.1.2
3
+ Version: 0.2.0
4
4
  Author-email: Kris Sankaran <ksankaran@wisc.edu>
5
5
  Requires-Python: >=3.10
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
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.1.2"
10
+ version = "0.2.0"
11
11
  description = ""
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
14
- "altair==6.0.0",
15
- "pyarrow==23.0.1",
16
- "scanpy==1.12",
17
- "scipy==1.15.2",
18
- "vegafusion==2.0.3",
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
- plot_dispersion_surface,
18
- plot_mean_surface,
19
- plot_spatial
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.1.2
3
+ Version: 0.2.0
4
4
  Author-email: Kris Sankaran <ksankaran@wisc.edu>
5
5
  Requires-Python: >=3.10
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
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/pseudotime.py
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
@@ -0,0 +1,10 @@
1
+ altair>=6.0.0
2
+ pyarrow>=23.0.1
3
+ scanpy>=1.12
4
+ scipy>=1.15.2
5
+ vegafusion>=2.0.3
6
+ vl-convert-python>=1.8.0
7
+
8
+ [dev]
9
+ pytest
10
+ ruff
@@ -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)
@@ -1,10 +0,0 @@
1
- altair==6.0.0
2
- pyarrow==23.0.1
3
- scanpy==1.12
4
- scipy==1.15.2
5
- vegafusion==2.0.3
6
- vl-convert-python>=1.8.0
7
-
8
- [dev]
9
- pytest
10
- ruff
File without changes
File without changes
File without changes