bs-python-utils 0.0.1__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.
- bs_python_utils/Timer.py +76 -0
- bs_python_utils/__init__.py +0 -0
- bs_python_utils/bs_altair.py +927 -0
- bs_python_utils/bs_logging.py +100 -0
- bs_python_utils/bs_mathstr.py +130 -0
- bs_python_utils/bs_mem.py +148 -0
- bs_python_utils/bs_opt.py +518 -0
- bs_python_utils/bs_plots.py +2 -0
- bs_python_utils/bs_seaborn.py +174 -0
- bs_python_utils/bs_sparse_gaussian.py +46 -0
- bs_python_utils/bsmplutils.py +34 -0
- bs_python_utils/bsnputils.py +957 -0
- bs_python_utils/bssputils.py +79 -0
- bs_python_utils/bsstats.py +463 -0
- bs_python_utils/bsutils.py +363 -0
- bs_python_utils/distance_covariances.py +258 -0
- bs_python_utils/example_opt.py +71 -0
- bs_python_utils/examples_altair.py +195 -0
- bs_python_utils/examples_distance_covariances.py +32 -0
- bs_python_utils/examples_mem.py +25 -0
- bs_python_utils/examples_seaborn.py +37 -0
- bs_python_utils/examples_sklearn.py +33 -0
- bs_python_utils/pandas_utils.py +239 -0
- bs_python_utils/sklearn_utils.py +74 -0
- bs_python_utils-0.0.1.dist-info/LICENSE +21 -0
- bs_python_utils-0.0.1.dist-info/METADATA +71 -0
- bs_python_utils-0.0.1.dist-info/RECORD +28 -0
- bs_python_utils-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
""" a personal library of Altair plots
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
import altair as alt
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from altair_saver import save as alt_save
|
|
10
|
+
|
|
11
|
+
from bs_python_utils.bsnputils import check_matrix, check_vector
|
|
12
|
+
from bs_python_utils.bsutils import bs_error_abort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _maybe_save(ch: alt.Chart, save: str | None = None):
|
|
16
|
+
if save is not None:
|
|
17
|
+
alt_save(ch, f"{save}.html")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _add_title(ch: alt.Chart, title: str | None = None) -> alt.Chart:
|
|
21
|
+
if title is not None:
|
|
22
|
+
if isinstance(title, str):
|
|
23
|
+
ch = ch.properties(title=title)
|
|
24
|
+
else:
|
|
25
|
+
bs_error_abort(f"title must be a string, not {title}")
|
|
26
|
+
return ch
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def alt_scatterplot(
|
|
30
|
+
df: pd.DataFrame,
|
|
31
|
+
str_x: str,
|
|
32
|
+
str_y: str,
|
|
33
|
+
time_series: bool = False,
|
|
34
|
+
save: str | None = None,
|
|
35
|
+
xlabel: str | None = None,
|
|
36
|
+
ylabel: str | None = None,
|
|
37
|
+
size: int | None = 30,
|
|
38
|
+
title: str | None = None,
|
|
39
|
+
color: str | None = None,
|
|
40
|
+
aggreg: str | None = None,
|
|
41
|
+
selection: bool = False,
|
|
42
|
+
) -> alt.Chart:
|
|
43
|
+
"""
|
|
44
|
+
scatterplot of `df[str_x]` vs `df[str_y]`
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
df: the data with columns for x, y
|
|
48
|
+
str_x: the name of a continuous x column
|
|
49
|
+
str_y: the name of a continuous y column
|
|
50
|
+
time_series: `True` if x is a time series
|
|
51
|
+
xlabel: label for the horizontal axis
|
|
52
|
+
ylabel: label for the vertical axis
|
|
53
|
+
title: title for the graph
|
|
54
|
+
size: radius of the circles
|
|
55
|
+
color: variable that determines the color of the circles
|
|
56
|
+
selection: if `True`, the user can select interactively from the `color` legend, if any
|
|
57
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
58
|
+
aggreg: the name of an aggregating function for `y`
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
the `alt.Chart` object
|
|
62
|
+
"""
|
|
63
|
+
type_x = "T" if time_series else "Q"
|
|
64
|
+
var_x = alt.X(f"{str_x}:{type_x}")
|
|
65
|
+
|
|
66
|
+
if xlabel is not None:
|
|
67
|
+
if isinstance(xlabel, str):
|
|
68
|
+
var_x = alt.X(f"{str_x}:{type_x}", axis=alt.Axis(title=xlabel))
|
|
69
|
+
else:
|
|
70
|
+
bs_error_abort(f"xlabel must be a string, not {xlabel}")
|
|
71
|
+
|
|
72
|
+
var_y = f"{aggreg}({str_y}):Q" if aggreg is not None else str_y
|
|
73
|
+
|
|
74
|
+
if ylabel is not None:
|
|
75
|
+
if isinstance(ylabel, str):
|
|
76
|
+
var_y = alt.Y(var_y, axis=alt.Axis(title=ylabel))
|
|
77
|
+
else:
|
|
78
|
+
bs_error_abort(f"ylabel must be a string, not {ylabel}")
|
|
79
|
+
|
|
80
|
+
if isinstance(size, int):
|
|
81
|
+
circles_size = size
|
|
82
|
+
else:
|
|
83
|
+
bs_error_abort(f"size must be an integer, not {size}")
|
|
84
|
+
|
|
85
|
+
if color is not None:
|
|
86
|
+
if isinstance(color, str):
|
|
87
|
+
if selection:
|
|
88
|
+
selection_criterion = alt.selection_multi(fields=[color], bind="legend")
|
|
89
|
+
ch = (
|
|
90
|
+
alt.Chart(df)
|
|
91
|
+
.mark_circle(size=circles_size)
|
|
92
|
+
.encode(
|
|
93
|
+
x=var_x,
|
|
94
|
+
y=var_y,
|
|
95
|
+
color=color,
|
|
96
|
+
opacity=alt.condition(
|
|
97
|
+
selection_criterion, alt.value(1), alt.value(0.1)
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
.add_selection(selection_criterion)
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
ch = (
|
|
104
|
+
alt.Chart(df)
|
|
105
|
+
.mark_circle(size=circles_size)
|
|
106
|
+
.encode(x=var_x, y=var_y, color=color)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
bs_error_abort(f"color must be a string, not {color}")
|
|
110
|
+
else:
|
|
111
|
+
ch = alt.Chart(df).mark_circle(size=circles_size).encode(x=var_x, y=var_y)
|
|
112
|
+
|
|
113
|
+
ch = _add_title(ch, title)
|
|
114
|
+
_maybe_save(ch, save)
|
|
115
|
+
return ch
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def alt_lineplot(
|
|
119
|
+
df: pd.DataFrame,
|
|
120
|
+
str_x: str,
|
|
121
|
+
str_y: str,
|
|
122
|
+
time_series: bool = False,
|
|
123
|
+
save: str | None = None,
|
|
124
|
+
aggreg: str | None = None,
|
|
125
|
+
**kwargs,
|
|
126
|
+
) -> alt.Chart:
|
|
127
|
+
"""
|
|
128
|
+
scatterplot of `df[str_x]` vs `df[str_y]`
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
df: the data with columns `str_x` and `str_y`
|
|
132
|
+
str_x: the name of a continuous column
|
|
133
|
+
str_y: the name of a continuous column
|
|
134
|
+
time_series: `True` if x is a time series
|
|
135
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
136
|
+
aggreg: the name of an aggregating function for `y`
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
the `alt.Chart` object
|
|
140
|
+
"""
|
|
141
|
+
type_x = "T" if time_series else "Q"
|
|
142
|
+
var_y = f"{aggreg}({str_y}):Q" if aggreg is not None else str_y
|
|
143
|
+
|
|
144
|
+
ch = alt.Chart(df).mark_line().encode(x=f"{str_x}:{type_x}", y=var_y)
|
|
145
|
+
if "title" in kwargs:
|
|
146
|
+
ch = ch.properties(title=kwargs["title"])
|
|
147
|
+
_maybe_save(ch, save)
|
|
148
|
+
return ch
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def alt_plot_fun(
|
|
152
|
+
f: Callable,
|
|
153
|
+
start: float,
|
|
154
|
+
end: float,
|
|
155
|
+
npoints: int = 100,
|
|
156
|
+
save: str | None = None,
|
|
157
|
+
) -> alt.Chart:
|
|
158
|
+
"""
|
|
159
|
+
plots the function `f` from `start` to `end`
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
f: returns a Numpy array from a Numpy array
|
|
163
|
+
start: first point on `x` axis
|
|
164
|
+
end: last point on `x` axis
|
|
165
|
+
npoints: number of points
|
|
166
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
the `alt.Chart` object
|
|
170
|
+
"""
|
|
171
|
+
step = (end - start) / npoints
|
|
172
|
+
points = np.arange(start, end + step, step)
|
|
173
|
+
fun_data = pd.DataFrame({"x": points, "y": f(points)})
|
|
174
|
+
|
|
175
|
+
ch = (
|
|
176
|
+
alt.Chart(fun_data)
|
|
177
|
+
.mark_line()
|
|
178
|
+
.encode(
|
|
179
|
+
x="x:Q",
|
|
180
|
+
y="y:Q",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
_maybe_save(ch, save)
|
|
185
|
+
return ch
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def alt_density(df: pd.DataFrame, str_x: str, save: str | None = None) -> alt.Chart:
|
|
189
|
+
"""plots the density of `df[str_x]`
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
df: the data with the `str_x` variable
|
|
193
|
+
str_x: the name of a continuous column
|
|
194
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
the `alt.Chart` object
|
|
198
|
+
"""
|
|
199
|
+
ch = (
|
|
200
|
+
alt.Chart(df)
|
|
201
|
+
.transform_density(
|
|
202
|
+
str_x,
|
|
203
|
+
as_=[str_x, "Density"],
|
|
204
|
+
)
|
|
205
|
+
.mark_area(opacity=0.4)
|
|
206
|
+
.encode(
|
|
207
|
+
x=f"{str_x}:Q",
|
|
208
|
+
y="Density:Q",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
_maybe_save(ch, save)
|
|
213
|
+
return ch
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def alt_linked_scatterplots(
|
|
217
|
+
df: pd.DataFrame,
|
|
218
|
+
str_x1: str,
|
|
219
|
+
str_x2: str,
|
|
220
|
+
str_y: str,
|
|
221
|
+
str_f: str,
|
|
222
|
+
save: str | None = None,
|
|
223
|
+
) -> alt.Chart:
|
|
224
|
+
"""
|
|
225
|
+
two scatterplots: of `df[str_x1]` vs `df[str_y]` and of `df[str_x2]` vs `df[str_y]`,
|
|
226
|
+
both with color as per `df[str_f]`. Selecting an interval in one shows up in the other.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
df:
|
|
230
|
+
str_x1: the name of a continuous column
|
|
231
|
+
str_x2: the name of a continuous column
|
|
232
|
+
str_y: the name of a continuous column
|
|
233
|
+
str_f: the name of a categorical column
|
|
234
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
the `alt.Chart` object
|
|
238
|
+
"""
|
|
239
|
+
interval = alt.selection_interval()
|
|
240
|
+
|
|
241
|
+
base = (
|
|
242
|
+
alt.Chart(df)
|
|
243
|
+
.mark_point()
|
|
244
|
+
.encode(
|
|
245
|
+
y=f"{str_y}:Q", color=alt.condition(interval, str_f, alt.value("lightgray"))
|
|
246
|
+
)
|
|
247
|
+
.properties(selection=interval)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
ch = base.encode(x=f"{str_x1}:Q") | base.encode(x=f"{str_x2}:Q")
|
|
251
|
+
|
|
252
|
+
_maybe_save(ch, save)
|
|
253
|
+
return ch
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def alt_scatterplot_with_histo(
|
|
257
|
+
df: pd.DataFrame, str_x: str, str_y: str, str_f: str, save: str | None = None
|
|
258
|
+
) -> alt.Chart:
|
|
259
|
+
"""
|
|
260
|
+
scatterplots `df[str_x]` vs `df[str_y]` with colors as per `df[str_f]`
|
|
261
|
+
allows to select an interval and histograns the counts of `df[str_f]` in the interval
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
df: the data with the `str_x` and `str_f` variables
|
|
265
|
+
str_x: the name of a continuous column
|
|
266
|
+
str_y: the name of a continuous column
|
|
267
|
+
str_f: the name of a categorical column
|
|
268
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
the `alt.Chart` object
|
|
272
|
+
"""
|
|
273
|
+
interval = alt.selection_interval()
|
|
274
|
+
|
|
275
|
+
points = (
|
|
276
|
+
alt.Chart(df)
|
|
277
|
+
.mark_point()
|
|
278
|
+
.encode(
|
|
279
|
+
x=f"{str_x}:Q",
|
|
280
|
+
y=f"{str_y}:Q",
|
|
281
|
+
color=alt.condition(interval, str_f, alt.value("lightgray")),
|
|
282
|
+
)
|
|
283
|
+
.properties(selection=interval)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
histogram = (
|
|
287
|
+
alt.Chart(df)
|
|
288
|
+
.mark_bar()
|
|
289
|
+
.encode(
|
|
290
|
+
x="count()",
|
|
291
|
+
y=str_f,
|
|
292
|
+
color=str_f,
|
|
293
|
+
)
|
|
294
|
+
.transform_filter(interval)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
ch = points & histogram
|
|
298
|
+
|
|
299
|
+
_maybe_save(ch, save)
|
|
300
|
+
return ch
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def alt_faceted_densities(
|
|
304
|
+
df: pd.DataFrame,
|
|
305
|
+
str_x: str,
|
|
306
|
+
str_f: str,
|
|
307
|
+
legend_title: str | None = None,
|
|
308
|
+
save: str | None = None,
|
|
309
|
+
max_cols: int | None = 4,
|
|
310
|
+
) -> alt.Chart:
|
|
311
|
+
"""
|
|
312
|
+
plots the density of `df[str_x]` by `df[str_f]` in column facets
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
df: the data with the `str_x` and `str_f` variables
|
|
316
|
+
str_x: the name of a continuous column
|
|
317
|
+
str_f: the name of a categorical column
|
|
318
|
+
legend_title: a title for the legend
|
|
319
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
320
|
+
max_cols: we wrap after that number of columns
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
the `alt.Chart` object
|
|
324
|
+
"""
|
|
325
|
+
our_legend_title = str_f if legend_title is None else legend_title
|
|
326
|
+
ch = (
|
|
327
|
+
alt.Chart(df)
|
|
328
|
+
.transform_density(
|
|
329
|
+
str_x,
|
|
330
|
+
groupby=[str_f],
|
|
331
|
+
as_=[str_x, "Density"],
|
|
332
|
+
)
|
|
333
|
+
.mark_area(opacity=0.4)
|
|
334
|
+
.encode(
|
|
335
|
+
x=f"{str_x}:Q",
|
|
336
|
+
y="Density:Q",
|
|
337
|
+
color=alt.Color(f"{str_f}:N", title=our_legend_title),
|
|
338
|
+
)
|
|
339
|
+
.facet(f"{str_f}:N", columns=max_cols)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
_maybe_save(ch, save)
|
|
343
|
+
return ch
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def alt_superposed_lineplot(
|
|
347
|
+
df: pd.DataFrame,
|
|
348
|
+
str_x: str,
|
|
349
|
+
str_y: str,
|
|
350
|
+
str_f: str,
|
|
351
|
+
time_series: bool = False,
|
|
352
|
+
legend_title: str | None = None,
|
|
353
|
+
save: str | None = None,
|
|
354
|
+
) -> alt.Chart:
|
|
355
|
+
"""
|
|
356
|
+
plots `df[str_x]` vs `df[str_y]` by `df[str_f]` on one plot
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
df: the data with the `str_x`, `str_y`, and `str_f` variables
|
|
360
|
+
str_x: the name of a continuous `x` column
|
|
361
|
+
str_y: the name of a continuous `y` column
|
|
362
|
+
str_f: the name of a categorical `f` column
|
|
363
|
+
time_series: `True` if `str_x` is a time series
|
|
364
|
+
legend_title: a title for the legend
|
|
365
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
the `alt.Chart` object
|
|
369
|
+
"""
|
|
370
|
+
type_x = "T" if time_series else "Q"
|
|
371
|
+
our_legend_title = str_f if legend_title is None else legend_title
|
|
372
|
+
ch = (
|
|
373
|
+
alt.Chart(df)
|
|
374
|
+
.mark_line()
|
|
375
|
+
.encode(
|
|
376
|
+
x=f"{str_x}:{type_x}",
|
|
377
|
+
y=f"{str_y}:Q",
|
|
378
|
+
color=alt.Color(f"{str_f}:N", title=our_legend_title),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
_maybe_save(ch, save)
|
|
382
|
+
return ch
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def alt_superposed_faceted_lineplot(
|
|
386
|
+
df: pd.DataFrame,
|
|
387
|
+
str_x: str,
|
|
388
|
+
str_y: str,
|
|
389
|
+
str_f: str,
|
|
390
|
+
str_g: str,
|
|
391
|
+
time_series: bool = False,
|
|
392
|
+
legend_title: str | None = None,
|
|
393
|
+
max_cols: int | None = 5,
|
|
394
|
+
save: str | None = None,
|
|
395
|
+
) -> alt.Chart:
|
|
396
|
+
"""
|
|
397
|
+
plots `df[str_x]` vs `df[str_y]` superposed by `df[str_f]` and faceted by `df[str_g]`
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
df: the data with the `str_x`, `str_y`, and `str_f` variables
|
|
401
|
+
str_x: the name of a continuous column
|
|
402
|
+
str_y: the name of a continuous column
|
|
403
|
+
str_f: the name of a categorical column
|
|
404
|
+
str_g: the name of a categorical column
|
|
405
|
+
time_series: `True` if `str_x` is a time series
|
|
406
|
+
legend_title: a title for the legend
|
|
407
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
408
|
+
max_cols: we wrap after that number of columns
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
the `alt.Chart` object
|
|
413
|
+
"""
|
|
414
|
+
type_x = "T" if time_series else "Q"
|
|
415
|
+
our_title = str_f if legend_title is None else legend_title
|
|
416
|
+
ch = (
|
|
417
|
+
alt.Chart(df)
|
|
418
|
+
.mark_line()
|
|
419
|
+
.encode(
|
|
420
|
+
x=f"{str_x}:{type_x}",
|
|
421
|
+
y=f"{str_y}:Q",
|
|
422
|
+
color=alt.Color(f"{str_f}:N", title=our_title),
|
|
423
|
+
facet=alt.Facet(f"{str_g}:N", columns=max_cols),
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
_maybe_save(ch, save)
|
|
427
|
+
return ch
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def alt_histogram_by(
|
|
431
|
+
df: pd.DataFrame,
|
|
432
|
+
str_x: str,
|
|
433
|
+
str_y: str,
|
|
434
|
+
str_agg: str | None = "mean",
|
|
435
|
+
save: str | None = None,
|
|
436
|
+
) -> alt.Chart:
|
|
437
|
+
"""
|
|
438
|
+
plots a histogram of a statistic of `str_y` by `str_x`
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
df: a dataframe with columns `str_x` and `str_y`
|
|
442
|
+
str_x: a categorical variable
|
|
443
|
+
str_y: a continuous variable
|
|
444
|
+
str_agg: how we aggregate the values of `str_y` by `str_x`
|
|
445
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
the Altair chart
|
|
449
|
+
"""
|
|
450
|
+
ch = (
|
|
451
|
+
alt.Chart(df)
|
|
452
|
+
.mark_bar()
|
|
453
|
+
.encode(x=str_x, y=f"{str_agg}({str_y}):Q")
|
|
454
|
+
.properties(height=300, width=400)
|
|
455
|
+
)
|
|
456
|
+
_maybe_save(ch, save)
|
|
457
|
+
return ch
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def alt_histogram_continuous(
|
|
461
|
+
df: pd.DataFrame, str_x: str, save: str | None = None
|
|
462
|
+
) -> alt.Chart:
|
|
463
|
+
"""
|
|
464
|
+
histogram of a continuous variable `df[str_x]`
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
df: the data with the `str_x`, `str_y`, and `str_f` variables
|
|
468
|
+
str_x: the name of a continuous column
|
|
469
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
the `alt.Chart` object
|
|
473
|
+
"""
|
|
474
|
+
ch = alt.Chart(df).mark_bar().encode(alt.X(str_x, bin=True), y="count()")
|
|
475
|
+
_maybe_save(ch, save)
|
|
476
|
+
return ch
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def alt_stacked_area(
|
|
480
|
+
df: pd.DataFrame,
|
|
481
|
+
str_x: str,
|
|
482
|
+
str_y: str,
|
|
483
|
+
str_f: str,
|
|
484
|
+
time_series: bool = False,
|
|
485
|
+
title: str | None = None,
|
|
486
|
+
save: str | None = None,
|
|
487
|
+
) -> alt.Chart:
|
|
488
|
+
"""
|
|
489
|
+
normalized stacked lineplots of `df[str_x]` vs `df[str_y]` by `df[str_f]`
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
df: the data with columns for `str_x`, `str_y`, and `str_f`
|
|
493
|
+
str_x: the name of a continuous column
|
|
494
|
+
str_y: the name of a continuous column
|
|
495
|
+
str_f: the name of a categorical column
|
|
496
|
+
time_series: `True` if `str_x` is a time series
|
|
497
|
+
title: a title for the plot
|
|
498
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
the `alt.Chart` object
|
|
502
|
+
"""
|
|
503
|
+
type_x = "T" if time_series else "Q"
|
|
504
|
+
ch = (
|
|
505
|
+
alt.Chart(df)
|
|
506
|
+
.mark_area()
|
|
507
|
+
.encode(
|
|
508
|
+
x=f"{str_x}:{type_x}",
|
|
509
|
+
y=alt.Y(f"{str_y}:Q", stack="normalize"),
|
|
510
|
+
color=f"{str_f}:N",
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
if title is not None:
|
|
514
|
+
ch = ch.properties(title=title)
|
|
515
|
+
|
|
516
|
+
_maybe_save(ch, save)
|
|
517
|
+
return ch
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def alt_stacked_area_facets(
|
|
521
|
+
df: pd.DataFrame,
|
|
522
|
+
str_x: str,
|
|
523
|
+
str_y: str,
|
|
524
|
+
str_f: str,
|
|
525
|
+
str_g: str,
|
|
526
|
+
time_series: bool = False,
|
|
527
|
+
max_cols: int | None = 5,
|
|
528
|
+
title: str | None = None,
|
|
529
|
+
save: str | None = None,
|
|
530
|
+
) -> alt.Chart:
|
|
531
|
+
"""
|
|
532
|
+
normalized stacked lineplots of `df[str_x]` vs `df[str_y]` by `df[str_f]`, faceted by `df[str_g]`
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
df: the data with columns for `str_x`, `str_y`, and `str_f`
|
|
536
|
+
str_x: the name of a continuous column
|
|
537
|
+
str_y: the name of a continuous column
|
|
538
|
+
str_f: the name of a categorical column
|
|
539
|
+
str_g: the name of a categorical column
|
|
540
|
+
time_series: `True` if `str_x` is a time series
|
|
541
|
+
title: a title for the plot
|
|
542
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
the `alt.Chart` object
|
|
546
|
+
"""
|
|
547
|
+
type_x = "T" if time_series else "Q"
|
|
548
|
+
ch = (
|
|
549
|
+
alt.Chart(df)
|
|
550
|
+
.mark_area()
|
|
551
|
+
.encode(
|
|
552
|
+
x=f"{str_x}:{type_x}",
|
|
553
|
+
y=alt.Y(f"{str_y}:Q", stack="normalize"),
|
|
554
|
+
color=f"{str_f}:N",
|
|
555
|
+
facet=alt.Facet(f"{str_g}:N", columns=max_cols),
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
_maybe_save(ch, save)
|
|
559
|
+
return ch
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _stack_estimates(
|
|
563
|
+
estimate_names: str | list[str], estimates: np.ndarray, df: pd.DataFrame
|
|
564
|
+
) -> tuple[pd.DataFrame, list[str]]:
|
|
565
|
+
"""
|
|
566
|
+
adds to a dataframe `df` columns with names `estimate_names` for various `estimates of one coefficient
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
estimate_names: names of the n estimate columns to be added
|
|
570
|
+
estimates: a matrix with n columns vectors
|
|
571
|
+
df: a receiving data frame
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
the dataframe, updated; and the names+['True value']
|
|
575
|
+
"""
|
|
576
|
+
df1 = df.copy()
|
|
577
|
+
n_estimates = 1 if isinstance(estimate_names, str) else len(estimate_names)
|
|
578
|
+
if n_estimates == 1:
|
|
579
|
+
size_est = check_vector(estimates, "_stack_estimates")
|
|
580
|
+
if size_est != n_estimates:
|
|
581
|
+
bs_error_abort(
|
|
582
|
+
f"_stack_estimates: we have {n_estimates} names of estimators and"
|
|
583
|
+
f" {size_est} estimators"
|
|
584
|
+
)
|
|
585
|
+
df1[estimate_names] = estimates
|
|
586
|
+
ordered_estimates = [estimate_names, "True value"]
|
|
587
|
+
else:
|
|
588
|
+
shape_est = check_matrix(estimates, "_stack_estimates")
|
|
589
|
+
if shape_est[1] != n_estimates:
|
|
590
|
+
bs_error_abort(
|
|
591
|
+
f"_stack_estimates: we have {n_estimates} names of estimators and"
|
|
592
|
+
f" {shape_est[1]} estimators"
|
|
593
|
+
)
|
|
594
|
+
for i_est, est_name in enumerate(estimate_names):
|
|
595
|
+
df1[est_name] = estimates[:, i_est]
|
|
596
|
+
ordered_estimates = [*estimate_names, "True value"]
|
|
597
|
+
|
|
598
|
+
return df1, ordered_estimates
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def plot_parameterized_estimates(
|
|
602
|
+
parameter_name: str,
|
|
603
|
+
parameter_values: np.ndarray,
|
|
604
|
+
coeff_names: str | list[str],
|
|
605
|
+
true_values: np.ndarray,
|
|
606
|
+
estimate_names: str | list[str],
|
|
607
|
+
estimates: np.ndarray,
|
|
608
|
+
colors: list[str],
|
|
609
|
+
save: str | None = None,
|
|
610
|
+
) -> alt.Chart:
|
|
611
|
+
"""
|
|
612
|
+
plots estimates of coefficients, with the true values, as a function of a parameter; one facet per coefficient
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
parameter_name: the name of the parameter
|
|
616
|
+
parameter_values: a vector of `n_vals` values for the parameter
|
|
617
|
+
coeff_names: the names of the `n_coeffs` coefficients
|
|
618
|
+
true_values: their true values, depending on the parameter or not
|
|
619
|
+
estimate_names: names of the estimates
|
|
620
|
+
estimates: their values
|
|
621
|
+
colors: colors for the various estimates
|
|
622
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
the `alt.Chart` object
|
|
626
|
+
"""
|
|
627
|
+
n_vals = check_vector(parameter_values)
|
|
628
|
+
n_coeffs = 1 if isinstance(coeff_names, str) else len(coeff_names)
|
|
629
|
+
if n_coeffs == 1:
|
|
630
|
+
n_true = check_vector(true_values, "plot_parameterized_estimates")
|
|
631
|
+
if n_true != n_vals:
|
|
632
|
+
bs_error_abort(
|
|
633
|
+
f"plot_parameterized_estimates: we have {n_true} values and"
|
|
634
|
+
f" {n_vals} parameter values."
|
|
635
|
+
)
|
|
636
|
+
df = pd.DataFrame({parameter_name: parameter_values, "True value": true_values})
|
|
637
|
+
df1, ordered_estimates = _stack_estimates(estimate_names, estimates, df)
|
|
638
|
+
df1m = pd.melt(df1, parameter_name, var_name="Estimate")
|
|
639
|
+
ch = (
|
|
640
|
+
alt.Chart(df1m)
|
|
641
|
+
.mark_line()
|
|
642
|
+
.encode(
|
|
643
|
+
x=f"{parameter_name}:Q",
|
|
644
|
+
y="value:Q",
|
|
645
|
+
strokeDash=alt.StrokeDash("Estimate:N", sort=ordered_estimates),
|
|
646
|
+
color=alt.Color(
|
|
647
|
+
"Estimate:N",
|
|
648
|
+
sort=estimate_names,
|
|
649
|
+
scale=alt.Scale(domain=ordered_estimates, range=colors),
|
|
650
|
+
),
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
else:
|
|
654
|
+
n_true, n_c = check_matrix(true_values, "plot_parameterized_estimates")
|
|
655
|
+
if n_true != n_vals:
|
|
656
|
+
bs_error_abort(
|
|
657
|
+
f"plot_parameterized_estimates: we have {n_true} true values and"
|
|
658
|
+
f" {n_vals} parameter values."
|
|
659
|
+
)
|
|
660
|
+
if n_c != n_coeffs:
|
|
661
|
+
bs_error_abort(
|
|
662
|
+
f"plot_parameterized_estimates: we have {n_c} columns of true values"
|
|
663
|
+
f" and {n_coeffs} coefficients."
|
|
664
|
+
)
|
|
665
|
+
df1 = [None] * n_coeffs
|
|
666
|
+
for i_coeff, coeff in enumerate(coeff_names):
|
|
667
|
+
df_i = pd.DataFrame(
|
|
668
|
+
{
|
|
669
|
+
parameter_name: parameter_values,
|
|
670
|
+
"True value": true_values[:, i_coeff],
|
|
671
|
+
}
|
|
672
|
+
)
|
|
673
|
+
df1[i_coeff], ordered_estimates = _stack_estimates(
|
|
674
|
+
estimate_names, estimates[..., i_coeff], df_i
|
|
675
|
+
)
|
|
676
|
+
df1[i_coeff]["Coefficient"] = coeff
|
|
677
|
+
|
|
678
|
+
df2 = pd.concat(df1[i_coeff] for i_coeff in range(n_coeffs))
|
|
679
|
+
ordered_colors = colors
|
|
680
|
+
df2m = pd.melt(df2, [parameter_name, "Coefficient"], var_name="Estimate")
|
|
681
|
+
ch = (
|
|
682
|
+
alt.Chart(df2m)
|
|
683
|
+
.mark_line()
|
|
684
|
+
.encode(
|
|
685
|
+
x=f"{parameter_name}:Q",
|
|
686
|
+
y="value:Q",
|
|
687
|
+
strokeDash=alt.StrokeDash("Estimate:N", sort=ordered_estimates),
|
|
688
|
+
color=alt.Color(
|
|
689
|
+
"Estimate:N",
|
|
690
|
+
sort=ordered_estimates,
|
|
691
|
+
scale=alt.Scale(domain=ordered_estimates, range=ordered_colors),
|
|
692
|
+
),
|
|
693
|
+
)
|
|
694
|
+
.facet(alt.Facet("Coefficient:N", sort=coeff_names))
|
|
695
|
+
.resolve_scale(y="independent")
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
_maybe_save(ch, save)
|
|
699
|
+
|
|
700
|
+
return ch
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def plot_true_sim_facets(
|
|
704
|
+
parameter_name: str,
|
|
705
|
+
parameter_values: np.ndarray,
|
|
706
|
+
stat_names: list[str],
|
|
707
|
+
stat_true: np.ndarray,
|
|
708
|
+
stat_sim: np.ndarray,
|
|
709
|
+
colors: list[str],
|
|
710
|
+
stat_title: str | None = "Statistic",
|
|
711
|
+
subtitle: str | None = "True vs estimated",
|
|
712
|
+
ncols: int | None = 3,
|
|
713
|
+
save: str | None = None,
|
|
714
|
+
) -> alt.Chart:
|
|
715
|
+
"""
|
|
716
|
+
plots simulated and true values of statistics as a function of a parameter; one facet per coefficient
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
parameter_name: the name of the parameter
|
|
720
|
+
parameter_values: a vector of `n_vals` values for the parameter
|
|
721
|
+
stat_names: the names of the `n` statistics
|
|
722
|
+
stat_true: their true values, `(n_vals, n)`
|
|
723
|
+
stat_sim: their simulated values
|
|
724
|
+
colors: colors for the various estimates
|
|
725
|
+
stat_title: main title
|
|
726
|
+
subtitle: subtitle
|
|
727
|
+
ncols: wrap after `ncols` columns
|
|
728
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
the `alt.Chart` object
|
|
732
|
+
"""
|
|
733
|
+
n_stats = len(stat_names)
|
|
734
|
+
nvals = check_vector(parameter_values, "plot_true_sim_facets")
|
|
735
|
+
nv_true, n_stat_true = check_matrix(stat_true, "plot_true_sim_facets")
|
|
736
|
+
if nv_true != nvals:
|
|
737
|
+
bs_error_abort(
|
|
738
|
+
f"plot_true_sim_facets: we have {nvals} parameter values and {nv_true} for"
|
|
739
|
+
" stat_true."
|
|
740
|
+
)
|
|
741
|
+
nv_est, n_stat_est = check_matrix(stat_sim, "plot_true_sim_facets")
|
|
742
|
+
if nv_est != nvals:
|
|
743
|
+
bs_error_abort(
|
|
744
|
+
f"plot_true_sim_facets: we have {nvals} parameter values and {nv_est} for"
|
|
745
|
+
" stat_sim."
|
|
746
|
+
)
|
|
747
|
+
if n_stat_true != n_stats:
|
|
748
|
+
bs_error_abort(
|
|
749
|
+
f"plot_true_sim_facets: we have {n_stats} names for {n_stat_true} true"
|
|
750
|
+
" statistics."
|
|
751
|
+
)
|
|
752
|
+
if n_stat_est != n_stats:
|
|
753
|
+
bs_error_abort(
|
|
754
|
+
f"plot_true_sim_facets: we have {n_stats} names for {n_stat_est} estimated"
|
|
755
|
+
" statistics."
|
|
756
|
+
)
|
|
757
|
+
df = pd.DataFrame(
|
|
758
|
+
{
|
|
759
|
+
parameter_name: parameter_values,
|
|
760
|
+
"True value": stat_true[:, 0],
|
|
761
|
+
"Estimated": stat_sim[:, 0],
|
|
762
|
+
stat_title: stat_names[0],
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
for i_stat in range(1, n_stats):
|
|
766
|
+
df_i = pd.DataFrame(
|
|
767
|
+
{
|
|
768
|
+
parameter_name: parameter_values,
|
|
769
|
+
"True value": stat_true[:, i_stat],
|
|
770
|
+
"Estimated": stat_sim[:, i_stat],
|
|
771
|
+
stat_title: stat_names[i_stat],
|
|
772
|
+
}
|
|
773
|
+
)
|
|
774
|
+
df = pd.concat((df, df_i))
|
|
775
|
+
sub_order = ["True value", "Estimated"]
|
|
776
|
+
dfm = pd.melt(df, [parameter_name, stat_title], var_name=subtitle)
|
|
777
|
+
ch = (
|
|
778
|
+
alt.Chart(dfm)
|
|
779
|
+
.mark_line()
|
|
780
|
+
.encode(
|
|
781
|
+
x=f"{parameter_name}:Q",
|
|
782
|
+
y="value:Q",
|
|
783
|
+
strokeDash=alt.StrokeDash(f"{subtitle}:N", sort=sub_order),
|
|
784
|
+
color=alt.Color(
|
|
785
|
+
f"{subtitle}:N",
|
|
786
|
+
sort=sub_order,
|
|
787
|
+
scale=alt.Scale(domain=sub_order, range=colors),
|
|
788
|
+
),
|
|
789
|
+
facet=alt.Facet(f"{stat_title}:N", sort=stat_names, columns=ncols),
|
|
790
|
+
)
|
|
791
|
+
.resolve_scale(y="independent")
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
_maybe_save(ch, save)
|
|
795
|
+
|
|
796
|
+
return ch
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def plot_true_sim2_facets(
|
|
800
|
+
parameter_name: str,
|
|
801
|
+
parameter_values: np.ndarray,
|
|
802
|
+
stat_names: list[str],
|
|
803
|
+
stat_true: np.ndarray,
|
|
804
|
+
stat_sim1: np.ndarray,
|
|
805
|
+
stat_sim2: np.ndarray,
|
|
806
|
+
colors: list[str],
|
|
807
|
+
stat_title: str | None = "Statistic",
|
|
808
|
+
subtitle: str | None = "True vs estimated",
|
|
809
|
+
ncols: int | None = 3,
|
|
810
|
+
save: str | None = None,
|
|
811
|
+
) -> alt.Chart:
|
|
812
|
+
"""
|
|
813
|
+
plots simulated values for two methods and true values of statistics as a function of a parameter;
|
|
814
|
+
one facet per coefficient
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
parameter_name: the name of the parameter
|
|
818
|
+
parameter_values: a vector of `n_vals` values for the parameter
|
|
819
|
+
stat_names: the names of the `n` statistics
|
|
820
|
+
stat_true: their true values, `(n_vals, n)`
|
|
821
|
+
stat_sim1: their simulated values, method 1
|
|
822
|
+
stat_sim2: their simulated values, method 2
|
|
823
|
+
colors: colors for the various estimates
|
|
824
|
+
stat_title: main title
|
|
825
|
+
subtitle: subtitle
|
|
826
|
+
ncols: wrap after `ncols` columns
|
|
827
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
the `alt.Chart` object
|
|
831
|
+
"""
|
|
832
|
+
n_stats = len(stat_names)
|
|
833
|
+
nvals = check_vector(parameter_values, "plot_true_sim2_facets")
|
|
834
|
+
nv_true, n_stat_true = check_matrix(stat_true, "plot_true_sim2_facets")
|
|
835
|
+
if nv_true != nvals:
|
|
836
|
+
bs_error_abort(f"we have {nvals} parameter values and {nv_true} for stat_true.")
|
|
837
|
+
if n_stat_true != n_stats:
|
|
838
|
+
bs_error_abort(f"we have {n_stats} names for {n_stat_true} true statistics.")
|
|
839
|
+
|
|
840
|
+
nv_est1, n_stat_est1 = check_matrix(stat_sim1, "plot_true_sim2_facets")
|
|
841
|
+
if nv_est1 != nvals:
|
|
842
|
+
bs_error_abort(f"we have {nvals} parameter values and {nv_est1} for stat_sim1.")
|
|
843
|
+
if n_stat_est1 != n_stats:
|
|
844
|
+
bs_error_abort(
|
|
845
|
+
f"we have {n_stats} names for {n_stat_est1} estimated statistics."
|
|
846
|
+
)
|
|
847
|
+
nv_est2, n_stat_est2 = check_matrix(stat_sim2, "plot_true_sim2_facets")
|
|
848
|
+
if nv_est2 != nvals:
|
|
849
|
+
bs_error_abort(f"we have {nvals} parameter values and {nv_est2} for stat_sim2.")
|
|
850
|
+
if n_stat_est2 != n_stats:
|
|
851
|
+
bs_error_abort(
|
|
852
|
+
f"we have {n_stats} names for {n_stat_est2} estimated statistics."
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
df = pd.DataFrame(
|
|
856
|
+
{
|
|
857
|
+
parameter_name: parameter_values,
|
|
858
|
+
"True value": stat_true[:, 0],
|
|
859
|
+
"Estimated1": stat_sim1[:, 0],
|
|
860
|
+
"Estimated2": stat_sim2[:, 0],
|
|
861
|
+
stat_title: stat_names[0],
|
|
862
|
+
}
|
|
863
|
+
)
|
|
864
|
+
for i_stat in range(1, n_stats):
|
|
865
|
+
df_i = pd.DataFrame(
|
|
866
|
+
{
|
|
867
|
+
parameter_name: parameter_values,
|
|
868
|
+
"True value": stat_true[:, i_stat],
|
|
869
|
+
"Estimated1": stat_sim1[:, i_stat],
|
|
870
|
+
"Estimated2": stat_sim2[:, i_stat],
|
|
871
|
+
stat_title: stat_names[i_stat],
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
df = pd.concat((df, df_i))
|
|
875
|
+
sub_order = ["True value", "Estimated1", "Estimated2"]
|
|
876
|
+
dfm = pd.melt(df, [parameter_name, stat_title], var_name=subtitle)
|
|
877
|
+
ch = (
|
|
878
|
+
alt.Chart(dfm)
|
|
879
|
+
.mark_line()
|
|
880
|
+
.encode(
|
|
881
|
+
x=f"{parameter_name}:Q",
|
|
882
|
+
y="value:Q",
|
|
883
|
+
strokeDash=alt.StrokeDash(f"{subtitle}:N", sort=sub_order),
|
|
884
|
+
color=alt.Color(
|
|
885
|
+
f"{subtitle}:N",
|
|
886
|
+
sort=sub_order,
|
|
887
|
+
scale=alt.Scale(domain=sub_order, range=colors),
|
|
888
|
+
),
|
|
889
|
+
facet=alt.Facet(f"{stat_title}:N", sort=stat_names, columns=ncols),
|
|
890
|
+
)
|
|
891
|
+
.resolve_scale(y="independent")
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
_maybe_save(ch, save)
|
|
895
|
+
|
|
896
|
+
return ch
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def alt_tick_plots(
|
|
900
|
+
df: pd.DataFrame, list_vars: str | list[str], save: str | None = None
|
|
901
|
+
) -> alt.Chart:
|
|
902
|
+
"""
|
|
903
|
+
ticks plot the `df` variables in `list_vars`, arranged vertically
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
df: a dataframe with the variables in `list_vars`
|
|
907
|
+
list_vars: the name of a column of `df`, or a list of names
|
|
908
|
+
save: the name of a file to save to (HTML extension will be added)
|
|
909
|
+
|
|
910
|
+
Returns:
|
|
911
|
+
the `alt.Chart` object
|
|
912
|
+
"""
|
|
913
|
+
if isinstance(list_vars, str):
|
|
914
|
+
varname = list_vars
|
|
915
|
+
ch = alt.Chart(df).encode(x=varname).mark_tick()
|
|
916
|
+
else:
|
|
917
|
+
ch = (
|
|
918
|
+
alt.Chart(df)
|
|
919
|
+
.encode(alt.X(alt.repeat("row"), type="quantitative"))
|
|
920
|
+
.mark_tick()
|
|
921
|
+
.repeat(row=list_vars)
|
|
922
|
+
.resolve_scale(y="independent")
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
_maybe_save(ch, save)
|
|
926
|
+
|
|
927
|
+
return ch
|