iqplot 0.3.8__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.
- iqplot/__init__.py +10 -0
- iqplot/cat.py +1838 -0
- iqplot/dist.py +2764 -0
- iqplot/utils.py +516 -0
- iqplot-0.3.8.dist-info/METADATA +41 -0
- iqplot-0.3.8.dist-info/RECORD +8 -0
- iqplot-0.3.8.dist-info/WHEEL +4 -0
- iqplot-0.3.8.dist-info/licenses/LICENSE +21 -0
iqplot/dist.py
ADDED
|
@@ -0,0 +1,2764 @@
|
|
|
1
|
+
"""Visualization of how data are distributed, split or colored by a
|
|
2
|
+
categorical variable."""
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
import colorcet
|
|
11
|
+
|
|
12
|
+
import bokeh.models
|
|
13
|
+
import bokeh.plotting
|
|
14
|
+
|
|
15
|
+
from . import utils
|
|
16
|
+
from . import cat
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import numba
|
|
20
|
+
|
|
21
|
+
njit = numba.njit
|
|
22
|
+
except:
|
|
23
|
+
njit = utils._dummy_jit
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ecdf(
|
|
27
|
+
data=None,
|
|
28
|
+
q=None,
|
|
29
|
+
cats=None,
|
|
30
|
+
q_axis="x",
|
|
31
|
+
palette=None,
|
|
32
|
+
order=None,
|
|
33
|
+
p=None,
|
|
34
|
+
show_legend=None,
|
|
35
|
+
legend_label=None,
|
|
36
|
+
legend_location="right",
|
|
37
|
+
legend_orientation="vertical",
|
|
38
|
+
legend_click_policy="hide",
|
|
39
|
+
tooltips=None,
|
|
40
|
+
complementary=False,
|
|
41
|
+
kind="collection",
|
|
42
|
+
style=None,
|
|
43
|
+
arrangement="overlay",
|
|
44
|
+
conf_int=False,
|
|
45
|
+
ptiles=(2.5, 97.5),
|
|
46
|
+
n_bs_reps=10_000,
|
|
47
|
+
marker="circle",
|
|
48
|
+
marker_kwargs=None,
|
|
49
|
+
line_kwargs=None,
|
|
50
|
+
fill_kwargs=None,
|
|
51
|
+
horizontal=None,
|
|
52
|
+
val=None,
|
|
53
|
+
click_policy=None,
|
|
54
|
+
conf_int_kwargs=None,
|
|
55
|
+
**kwargs,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Make an ECDF plot.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
data : Pandas DataFrame, 1D Numpy array, or xarray
|
|
63
|
+
DataFrame containing tidy data for plotting. If a Numpy array,
|
|
64
|
+
a single category is assumed and an ECDF generated from data.
|
|
65
|
+
q : hashable
|
|
66
|
+
Name of column to use as quantitative variable if `data` is a
|
|
67
|
+
Pandas DataFrame. Otherwise, `q` is used as the quantitative
|
|
68
|
+
axis label.
|
|
69
|
+
cats : hashable or list of hashables
|
|
70
|
+
Name of column(s) to use as categorical variable(s).
|
|
71
|
+
q_axis : str, either 'x' or 'y', default 'x'
|
|
72
|
+
Axis along which the quantitative value varies.
|
|
73
|
+
palette : list colors, or single color string
|
|
74
|
+
If a list, color palette to use. If a single string representing
|
|
75
|
+
a color, all glyphs are colored with that color. Default is
|
|
76
|
+
colorcet.b_glasbey_category10 from the colorcet package.
|
|
77
|
+
order : list or None
|
|
78
|
+
If not None, must be a list of unique group names when the input
|
|
79
|
+
data frame is grouped by `cats`. The order of the list specifies
|
|
80
|
+
the ordering of the categorical variables in the legend. If
|
|
81
|
+
None, the categories appear in the order in which they appeared
|
|
82
|
+
in the inputted data frame.
|
|
83
|
+
p : bokeh.plotting.Figure instance, or None (default)
|
|
84
|
+
If None, create a new figure. Otherwise, populate the existing
|
|
85
|
+
figure `p`.
|
|
86
|
+
show_legend : bool, default False
|
|
87
|
+
If True, display legend.
|
|
88
|
+
legend_label : str, default None
|
|
89
|
+
If `cats` is None and `show_legend` is True, then if
|
|
90
|
+
`legend_label` is not None, a legend is created for the glyph
|
|
91
|
+
on the plot and labeled with `legend_label`. Otherwise, no
|
|
92
|
+
legend is created if `cats` is None.
|
|
93
|
+
legend_location : str, default 'right'
|
|
94
|
+
Location of legend. If one of "right", "left", "above", or
|
|
95
|
+
"below", the legend is placed outside of the plot area. If one
|
|
96
|
+
of "top_left", "top_center", "top_right", "center_right",
|
|
97
|
+
"bottom_right", "bottom_center", "bottom_left", "center_left",
|
|
98
|
+
or "center", the legend is placed within the plot area. If a
|
|
99
|
+
2-tuple, legend is placed according to the coordinates in the
|
|
100
|
+
tuple.
|
|
101
|
+
legend_orientation : str, default 'vertical'
|
|
102
|
+
Either 'horizontal' or 'vertical'.
|
|
103
|
+
legend_click_policy : str, default 'hide'
|
|
104
|
+
Either 'hide', 'mute', or None; how the glyphs respond when the
|
|
105
|
+
corresponding category is clicked in the legend.
|
|
106
|
+
tooltips : list of 2-tuples
|
|
107
|
+
Specification for tooltips as per Bokeh specifications. For
|
|
108
|
+
example, if we want `col1` and `col2` tooltips, we can use
|
|
109
|
+
`tooltips=[('label 1', '@col1'), ('label 2', '@col2')]`.
|
|
110
|
+
complementary : bool, default False
|
|
111
|
+
If True, plot the empirical complementary cumulative
|
|
112
|
+
distribution function.
|
|
113
|
+
kind : str, default 'collection'
|
|
114
|
+
If 'collection', the figure is populated with a collection of
|
|
115
|
+
ECDFs coded with colors based on the categorical variables. If
|
|
116
|
+
'colored', the figure is populated with a single ECDF with
|
|
117
|
+
circles colored based on the categorical variables.
|
|
118
|
+
style : str, default 'staircase' for collection, 'dots' for colored
|
|
119
|
+
The style of ECDF to make.
|
|
120
|
+
|
|
121
|
+
- dots: Each data point is plotted as a dot.
|
|
122
|
+
- staircase: ECDF is plotted as a traditional staircase.
|
|
123
|
+
- formal: Strictly adhere to the definition of an ECDF.
|
|
124
|
+
arrangement : 'overlay' or 'stack', default 'overlay'
|
|
125
|
+
Arrangement of ECDFs. If 'overlay', all ECDFs are plotted on the
|
|
126
|
+
same set of axes. If 'stack', each category gets its own set of
|
|
127
|
+
axes, and the plots are stacked one on top of the other (or side
|
|
128
|
+
by side if `q_axis` is 'y'). Stacking requires `cats` to be
|
|
129
|
+
given and `kind` to be 'collection', and the returned object is
|
|
130
|
+
a Bokeh gridplot, not a figure.
|
|
131
|
+
conf_int : bool, default False
|
|
132
|
+
If True, display confidence interval of ECDF.
|
|
133
|
+
ptiles : list, default (2.5, 97.5)
|
|
134
|
+
The percentiles to use for the confidence interval. Ignored if
|
|
135
|
+
`conf_int` is False.
|
|
136
|
+
n_bs_reps : int, default 10,000
|
|
137
|
+
Number of bootstrap replicates to do to compute confidence
|
|
138
|
+
interval. Ignored if `conf_int` is False.
|
|
139
|
+
marker : str, default 'circle'
|
|
140
|
+
Name of marker to be used in the plot (ignored if `style` is
|
|
141
|
+
'staircase'). Must be one of ['asterisk', 'circle',
|
|
142
|
+
'circle_cross', 'circle_x', 'cross', 'dash', 'diamond',
|
|
143
|
+
'diamond_cross', 'hex', 'inverted_triangle', 'square',
|
|
144
|
+
'square_cross', 'square_x', 'triangle', 'x'].
|
|
145
|
+
marker_kwargs : dict
|
|
146
|
+
Keyword arguments to be passed to `p.scatter()` or other relevant
|
|
147
|
+
marker function.
|
|
148
|
+
line_kwargs : dict
|
|
149
|
+
Kwargs to be passed to `p.line()`, `p.ray()`, and `p.segment()`.
|
|
150
|
+
fill_kwargs : dict
|
|
151
|
+
Keyword arguments to pass to `p.patch()` when making the
|
|
152
|
+
confidence intervals for an ECDF. Ignored when `conf_int` is
|
|
153
|
+
False. By default {"fill_alpha": 0.3, "line_alpha": 0}.
|
|
154
|
+
horizontal : bool or None, default None
|
|
155
|
+
Deprecated. Use `q_axis`.
|
|
156
|
+
val : hashable
|
|
157
|
+
Deprecated, use `q`.
|
|
158
|
+
click_policy : str, default 'hide'
|
|
159
|
+
Deprecated. Use `legend_click_policy`.
|
|
160
|
+
conf_int_kwargs : dict
|
|
161
|
+
Deprecated. Use `fill_kwargs`.
|
|
162
|
+
kwargs
|
|
163
|
+
Any kwargs to be passed to `bokeh.plotting.figure()` when making
|
|
164
|
+
the plot.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
output : bokeh.plotting.Figure instance
|
|
169
|
+
Plot populated with ECDFs.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# In the future, we will have a max_points kwarg that will lead to
|
|
173
|
+
# thinning of the ECDF. The doc string for the parameter is:
|
|
174
|
+
# max_points : int or None, default None, NOT YET IMPLEMENTED
|
|
175
|
+
# Maximum number of data points to use in a single ECDF. If the
|
|
176
|
+
# number of data points exceeds max_points, then the plot is
|
|
177
|
+
# constructed where points corresponding to the quantiles
|
|
178
|
+
# [1, 2, ..., max_points] / max_points are plotted. The upper
|
|
179
|
+
# and lower extreme points are always included. If this thinning
|
|
180
|
+
# is applied, then the resulting plotted ECDF will always lie
|
|
181
|
+
# slightly below the non-thinned ECDF. If None and the number of
|
|
182
|
+
# points for an ECDF is greater than 2000, there will be a
|
|
183
|
+
# warning that the number of points exceeds the maximum. The
|
|
184
|
+
# warning will not be issued if max_points is an `int`.
|
|
185
|
+
|
|
186
|
+
# Protect against mutability of dicts
|
|
187
|
+
marker_kwargs = copy.copy(marker_kwargs)
|
|
188
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
189
|
+
fill_kwargs = copy.copy(fill_kwargs)
|
|
190
|
+
|
|
191
|
+
# Check to make sure kind is ok
|
|
192
|
+
if kind not in ["collection", "colored"]:
|
|
193
|
+
raise RuntimeError("`kind` must be in `['collection', 'colored']")
|
|
194
|
+
|
|
195
|
+
# Determine style
|
|
196
|
+
if style is None:
|
|
197
|
+
style = "staircase" if kind == "collection" else "dots"
|
|
198
|
+
|
|
199
|
+
# Check style
|
|
200
|
+
if style not in ["staircase", "dots", "formal"]:
|
|
201
|
+
raise RuntimeError("`style` must be one of 'staircase', 'dots', 'formal'.")
|
|
202
|
+
|
|
203
|
+
if conf_int:
|
|
204
|
+
if not isinstance(ptiles, (list, tuple, np.ndarray)) or len(ptiles) != 2:
|
|
205
|
+
raise RuntimeError("`ptiles` must be a list or tuple of length 2.")
|
|
206
|
+
else:
|
|
207
|
+
ptiles = np.sort(ptiles)
|
|
208
|
+
|
|
209
|
+
q, legend_click_policy, fill_kwargs = utils._parse_deprecations(
|
|
210
|
+
q,
|
|
211
|
+
q_axis,
|
|
212
|
+
val,
|
|
213
|
+
horizontal,
|
|
214
|
+
"y",
|
|
215
|
+
click_policy,
|
|
216
|
+
legend_click_policy,
|
|
217
|
+
conf_int_kwargs,
|
|
218
|
+
fill_kwargs,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if style == "formal" and complementary:
|
|
222
|
+
raise NotImplementedError("Complementary formal ECDFs not yet implemented.")
|
|
223
|
+
|
|
224
|
+
if palette is None:
|
|
225
|
+
palette = colorcet.b_glasbey_category10
|
|
226
|
+
elif isinstance(palette, str):
|
|
227
|
+
palette = [palette]
|
|
228
|
+
|
|
229
|
+
utils._check_cats_none(cats, order, show_legend, legend_label)
|
|
230
|
+
|
|
231
|
+
if arrangement == "stack":
|
|
232
|
+
if kind != "collection":
|
|
233
|
+
raise RuntimeError("Must have kind='collection' if arrangment='stack'.")
|
|
234
|
+
|
|
235
|
+
if show_legend is None:
|
|
236
|
+
show_legend = False
|
|
237
|
+
|
|
238
|
+
if show_legend:
|
|
239
|
+
warnings.warn(
|
|
240
|
+
"Cannot show legend with arrangement='stack'. There is no legend to show."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if cats is not None:
|
|
244
|
+
# Convert the data and `order` here as well as in the call to
|
|
245
|
+
# `ecdf()` for each panel so that the panels can be ordered.
|
|
246
|
+
data, q, cats, _ = utils._data_cats(
|
|
247
|
+
data, q, cats, show_legend, legend_label
|
|
248
|
+
)
|
|
249
|
+
order = utils._order_to_str(order)
|
|
250
|
+
|
|
251
|
+
# Check inputs here as well; `order` is not passed on to the
|
|
252
|
+
# call to `ecdf()` for each panel, so it is not checked there.
|
|
253
|
+
cats, _ = utils._check_cat_input(
|
|
254
|
+
data, cats, q, None, None, tooltips, palette, order, marker_kwargs
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return _stacked_ecdfs(
|
|
258
|
+
data,
|
|
259
|
+
q=q,
|
|
260
|
+
cats=cats,
|
|
261
|
+
q_axis=q_axis,
|
|
262
|
+
palette=palette,
|
|
263
|
+
order=order,
|
|
264
|
+
tooltips=tooltips,
|
|
265
|
+
complementary=complementary,
|
|
266
|
+
kind=kind,
|
|
267
|
+
style=style,
|
|
268
|
+
conf_int=conf_int,
|
|
269
|
+
ptiles=ptiles,
|
|
270
|
+
n_bs_reps=n_bs_reps,
|
|
271
|
+
marker=marker,
|
|
272
|
+
marker_kwargs=marker_kwargs,
|
|
273
|
+
line_kwargs=line_kwargs,
|
|
274
|
+
fill_kwargs=fill_kwargs,
|
|
275
|
+
**kwargs,
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
if show_legend is None:
|
|
279
|
+
show_legend = True
|
|
280
|
+
|
|
281
|
+
data, q, cats, show_legend = utils._data_cats(
|
|
282
|
+
data, q, cats, show_legend, legend_label
|
|
283
|
+
)
|
|
284
|
+
order = utils._order_to_str(order)
|
|
285
|
+
|
|
286
|
+
cats, cols = utils._check_cat_input(
|
|
287
|
+
data, cats, q, None, None, tooltips, palette, order, marker_kwargs
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
kwargs = utils._fig_dimensions(kwargs)
|
|
291
|
+
|
|
292
|
+
non_q_axis = "y" if q_axis == "x" else "x"
|
|
293
|
+
if (
|
|
294
|
+
conf_int
|
|
295
|
+
and f"{non_q_axis}_axis_type" in kwargs
|
|
296
|
+
and kwargs[f"{non_q_axis}_axis_type"] == "log"
|
|
297
|
+
):
|
|
298
|
+
warnings.warn(
|
|
299
|
+
f"Cannot reliably draw confidence intervals with a {non_q_axis}-axis on a log scale because zero cannot be represented. Omitting confidence interval."
|
|
300
|
+
)
|
|
301
|
+
conf_int = False
|
|
302
|
+
if (
|
|
303
|
+
conf_int
|
|
304
|
+
and f"{q_axis}_axis_type" in kwargs
|
|
305
|
+
and kwargs[f"{q_axis}_axis_type"] == "log"
|
|
306
|
+
and (data[q] <= 0).any()
|
|
307
|
+
):
|
|
308
|
+
warnings.warn(
|
|
309
|
+
f"Cannot draw confidence intervals with a {q_axis}-axis on a log scale because some values are negative. Any negative values will be omitted from the ECDF."
|
|
310
|
+
)
|
|
311
|
+
conf_int = False
|
|
312
|
+
|
|
313
|
+
if marker_kwargs is None:
|
|
314
|
+
marker_kwargs = {}
|
|
315
|
+
if line_kwargs is None:
|
|
316
|
+
line_kwargs = {}
|
|
317
|
+
|
|
318
|
+
# Change any kwarg of "color" to line_color and fill_color
|
|
319
|
+
marker_kwargs = utils._specific_fill_and_color_kwargs(marker_kwargs, "marker")
|
|
320
|
+
line_kwargs = utils._specific_fill_and_color_kwargs(line_kwargs, "line")
|
|
321
|
+
|
|
322
|
+
y = "__ECCDF" if complementary else "__ECDF"
|
|
323
|
+
|
|
324
|
+
if q_axis == "y":
|
|
325
|
+
if "x_axis_label" not in kwargs:
|
|
326
|
+
if complementary:
|
|
327
|
+
kwargs["x_axis_label"] = "ECCDF"
|
|
328
|
+
else:
|
|
329
|
+
kwargs["x_axis_label"] = "ECDF"
|
|
330
|
+
else:
|
|
331
|
+
if "y_axis_label" not in kwargs:
|
|
332
|
+
if complementary:
|
|
333
|
+
kwargs["y_axis_label"] = "ECCDF"
|
|
334
|
+
else:
|
|
335
|
+
kwargs["y_axis_label"] = "ECDF"
|
|
336
|
+
|
|
337
|
+
if q_axis == "y":
|
|
338
|
+
if "y_axis_label" not in kwargs:
|
|
339
|
+
kwargs["y_axis_label"] = q
|
|
340
|
+
else:
|
|
341
|
+
if "x_axis_label" not in kwargs:
|
|
342
|
+
kwargs["x_axis_label"] = q
|
|
343
|
+
|
|
344
|
+
if style in ["formal", "staircase"] and "line_width" not in line_kwargs:
|
|
345
|
+
line_kwargs["line_width"] = 2
|
|
346
|
+
|
|
347
|
+
if fill_kwargs is None:
|
|
348
|
+
fill_kwargs = {}
|
|
349
|
+
if "alpha" not in fill_kwargs and "fill_alpha" not in fill_kwargs:
|
|
350
|
+
fill_kwargs["fill_alpha"] = 0.3
|
|
351
|
+
if (
|
|
352
|
+
"line_alpha" not in fill_kwargs
|
|
353
|
+
and "alpha" not in fill_kwargs
|
|
354
|
+
and "line_color" not in fill_kwargs
|
|
355
|
+
):
|
|
356
|
+
fill_kwargs["line_alpha"] = 0
|
|
357
|
+
|
|
358
|
+
df = data.copy()
|
|
359
|
+
|
|
360
|
+
if kind == "collection":
|
|
361
|
+
if style == "dots" or tooltips is not None:
|
|
362
|
+
df[y] = df.groupby(cats)[q].transform(_ecdf_y, complementary=complementary)
|
|
363
|
+
elif kind == "colored":
|
|
364
|
+
df[y] = df[q].transform(_ecdf_y, complementary=complementary)
|
|
365
|
+
cols += [y]
|
|
366
|
+
|
|
367
|
+
_, df["__label"] = utils._source_and_labels_from_cats(df, cats)
|
|
368
|
+
cols += ["__label"]
|
|
369
|
+
|
|
370
|
+
df = _sort_df(df, cats, order)
|
|
371
|
+
|
|
372
|
+
if p is None:
|
|
373
|
+
p = bokeh.plotting.figure(**kwargs)
|
|
374
|
+
|
|
375
|
+
if style == "dots":
|
|
376
|
+
marker = utils._check_marker(marker)
|
|
377
|
+
|
|
378
|
+
if tooltips is not None:
|
|
379
|
+
p.add_tools(bokeh.models.HoverTool(tooltips=tooltips, name="hover_glyphs"))
|
|
380
|
+
|
|
381
|
+
fill_fill_color_supplied = "fill_color" in fill_kwargs
|
|
382
|
+
marker_fill_color_supplied = "fill_color" in marker_kwargs
|
|
383
|
+
marker_line_color_supplied = "line_color" in marker_kwargs
|
|
384
|
+
line_line_color_supplied = "line_color" in line_kwargs
|
|
385
|
+
fill_fill_color_supplied = "fill_color" in fill_kwargs
|
|
386
|
+
|
|
387
|
+
markers = []
|
|
388
|
+
lines = []
|
|
389
|
+
circles_high = []
|
|
390
|
+
circles_low = []
|
|
391
|
+
rays_high = []
|
|
392
|
+
rays_low = []
|
|
393
|
+
patches = []
|
|
394
|
+
labels = []
|
|
395
|
+
invisible_markers = []
|
|
396
|
+
|
|
397
|
+
if kind == "collection":
|
|
398
|
+
# Explicitly loop to enable click policies on the legend
|
|
399
|
+
# (not possible with factors)
|
|
400
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
401
|
+
labels.append(g["__label"].iloc[0])
|
|
402
|
+
if conf_int:
|
|
403
|
+
if not fill_fill_color_supplied:
|
|
404
|
+
fill_kwargs["fill_color"] = palette[i % len(palette)]
|
|
405
|
+
p, patch = _ecdf_conf_int(
|
|
406
|
+
p,
|
|
407
|
+
g[q],
|
|
408
|
+
complementary=complementary,
|
|
409
|
+
q_axis=q_axis,
|
|
410
|
+
n_bs_reps=n_bs_reps,
|
|
411
|
+
ptiles=ptiles,
|
|
412
|
+
**fill_kwargs,
|
|
413
|
+
)
|
|
414
|
+
patches.append(patch)
|
|
415
|
+
|
|
416
|
+
if not marker_line_color_supplied:
|
|
417
|
+
marker_kwargs["line_color"] = palette[i % len(palette)]
|
|
418
|
+
if not marker_fill_color_supplied:
|
|
419
|
+
marker_kwargs["fill_color"] = palette[i % len(palette)]
|
|
420
|
+
if not line_line_color_supplied:
|
|
421
|
+
line_kwargs["line_color"] = palette[i % len(palette)]
|
|
422
|
+
|
|
423
|
+
if style == "staircase":
|
|
424
|
+
p, new_line, new_ray_high, new_ray_low = _staircase_ecdf(
|
|
425
|
+
p,
|
|
426
|
+
data=g[q],
|
|
427
|
+
complementary=complementary,
|
|
428
|
+
q_axis=q_axis,
|
|
429
|
+
line_kwargs=line_kwargs,
|
|
430
|
+
)
|
|
431
|
+
lines.append(new_line)
|
|
432
|
+
rays_high.append(new_ray_high)
|
|
433
|
+
rays_low.append(new_ray_low)
|
|
434
|
+
|
|
435
|
+
if style == "dots":
|
|
436
|
+
if "name" not in marker_kwargs and tooltips is not None:
|
|
437
|
+
marker_kwargs["name"] = "hover_glyphs"
|
|
438
|
+
|
|
439
|
+
if q_axis == "y":
|
|
440
|
+
markers.append(
|
|
441
|
+
p.scatter(source=g, x=y, y=q, marker=marker, **marker_kwargs)
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
markers.append(
|
|
445
|
+
p.scatter(source=g, x=q, y=y, marker=marker, **marker_kwargs)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if style == "formal":
|
|
449
|
+
(
|
|
450
|
+
p,
|
|
451
|
+
circle,
|
|
452
|
+
segment,
|
|
453
|
+
new_ray_high,
|
|
454
|
+
new_ray_low,
|
|
455
|
+
new_circle_high,
|
|
456
|
+
new_circle_low,
|
|
457
|
+
) = _formal_ecdf(
|
|
458
|
+
p,
|
|
459
|
+
data=g[q],
|
|
460
|
+
complementary=complementary,
|
|
461
|
+
q_axis=q_axis,
|
|
462
|
+
marker_kwargs=marker_kwargs,
|
|
463
|
+
line_kwargs=line_kwargs,
|
|
464
|
+
)
|
|
465
|
+
markers.append(circle)
|
|
466
|
+
lines.append(segment)
|
|
467
|
+
rays_high.append(new_ray_high)
|
|
468
|
+
rays_low.append(new_ray_low)
|
|
469
|
+
circles_high.append(new_circle_high)
|
|
470
|
+
circles_low.append(new_circle_low)
|
|
471
|
+
|
|
472
|
+
# Add transparent dots for hovering
|
|
473
|
+
if style != "dots" and tooltips is not None:
|
|
474
|
+
if q_axis == "y":
|
|
475
|
+
invisible_markers.append(
|
|
476
|
+
p.scatter(
|
|
477
|
+
source=g,
|
|
478
|
+
x=y,
|
|
479
|
+
y=q,
|
|
480
|
+
name="hover_glyphs",
|
|
481
|
+
fill_alpha=0,
|
|
482
|
+
line_alpha=0,
|
|
483
|
+
size=7,
|
|
484
|
+
marker="circle",
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
invisible_markers.append(
|
|
489
|
+
p.scatter(
|
|
490
|
+
source=g,
|
|
491
|
+
x=q,
|
|
492
|
+
y=y,
|
|
493
|
+
name="hover_glyphs",
|
|
494
|
+
fill_alpha=0,
|
|
495
|
+
line_alpha=0,
|
|
496
|
+
size=7,
|
|
497
|
+
marker="circle",
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
elif kind == "colored":
|
|
501
|
+
if style in ["formal", "staircase"]:
|
|
502
|
+
raise RuntimeError(
|
|
503
|
+
"Cannot have a formal or staircase ECDF with `kind='colored'`."
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if conf_int:
|
|
507
|
+
if "fill_color" not in fill_kwargs:
|
|
508
|
+
fill_kwargs["fill_color"] = "gray"
|
|
509
|
+
|
|
510
|
+
p, patch = _ecdf_conf_int(
|
|
511
|
+
p,
|
|
512
|
+
df[q],
|
|
513
|
+
complementary=complementary,
|
|
514
|
+
q_axis=q_axis,
|
|
515
|
+
n_bs_reps=n_bs_reps,
|
|
516
|
+
ptiles=ptiles,
|
|
517
|
+
**fill_kwargs,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
y = "__ECCDF" if complementary else "__ECDF"
|
|
521
|
+
|
|
522
|
+
# Explicitly loop to enable click policies on the legend (not possible with factors)
|
|
523
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
524
|
+
source = bokeh.models.ColumnDataSource(g[cols])
|
|
525
|
+
mkwargs = marker_kwargs
|
|
526
|
+
mkwargs["color"] = palette[i % len(palette)]
|
|
527
|
+
labels.append(g["__label"].iloc[0])
|
|
528
|
+
if q_axis == "y":
|
|
529
|
+
markers.append(
|
|
530
|
+
p.scatter(source=source, x=y, y=q, marker=marker, **mkwargs)
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
markers.append(
|
|
534
|
+
p.scatter(source=source, x=q, y=y, marker=marker, **mkwargs)
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return _dist_legend(
|
|
538
|
+
p,
|
|
539
|
+
show_legend,
|
|
540
|
+
legend_location,
|
|
541
|
+
legend_orientation,
|
|
542
|
+
legend_click_policy,
|
|
543
|
+
labels,
|
|
544
|
+
markers,
|
|
545
|
+
lines,
|
|
546
|
+
patches,
|
|
547
|
+
rays_high,
|
|
548
|
+
rays_low,
|
|
549
|
+
circles_high,
|
|
550
|
+
circles_low,
|
|
551
|
+
invisible_markers,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def histogram(
|
|
556
|
+
data=None,
|
|
557
|
+
q=None,
|
|
558
|
+
cats=None,
|
|
559
|
+
palette=None,
|
|
560
|
+
order=None,
|
|
561
|
+
q_axis="x",
|
|
562
|
+
p=None,
|
|
563
|
+
rug=None,
|
|
564
|
+
rug_height=None,
|
|
565
|
+
show_legend=None,
|
|
566
|
+
legend_label=None,
|
|
567
|
+
legend_location="right",
|
|
568
|
+
legend_orientation="vertical",
|
|
569
|
+
legend_click_policy="hide",
|
|
570
|
+
tooltips=None,
|
|
571
|
+
bins="freedman-diaconis",
|
|
572
|
+
density=False,
|
|
573
|
+
style=None,
|
|
574
|
+
arrangement=None,
|
|
575
|
+
mirror=False,
|
|
576
|
+
hist_height=0.75,
|
|
577
|
+
conf_int=False,
|
|
578
|
+
ptiles=(2.5, 97.5),
|
|
579
|
+
n_bs_reps=10000,
|
|
580
|
+
line_kwargs=None,
|
|
581
|
+
fill_kwargs=None,
|
|
582
|
+
binsep_kwargs=None,
|
|
583
|
+
rug_kwargs=None,
|
|
584
|
+
conf_int_kwargs=None,
|
|
585
|
+
horizontal=None,
|
|
586
|
+
val=None,
|
|
587
|
+
click_policy=None,
|
|
588
|
+
kind=None,
|
|
589
|
+
**kwargs,
|
|
590
|
+
):
|
|
591
|
+
"""
|
|
592
|
+
Make a plot of histograms.
|
|
593
|
+
|
|
594
|
+
Parameters
|
|
595
|
+
----------
|
|
596
|
+
data : Pandas DataFrame, 1D Numpy array, or xarray
|
|
597
|
+
DataFrame containing tidy data for plotting. If a Numpy array,
|
|
598
|
+
a single category is assumed and a histogram generated from
|
|
599
|
+
data.
|
|
600
|
+
q : hashable
|
|
601
|
+
Name of column to use as quantitative variable if `data` is a
|
|
602
|
+
Pandas DataFrame. Otherwise, `q` is used as the quantitative
|
|
603
|
+
axis label.
|
|
604
|
+
cats : hashable or list of hashables
|
|
605
|
+
Name of column(s) to use as categorical variable(s).
|
|
606
|
+
q_axis : str, either 'x' or 'y', default 'x'
|
|
607
|
+
Axis along which the quantitative value varies.
|
|
608
|
+
palette : list colors, or single color string
|
|
609
|
+
If a list, color palette to use. If a single string representing
|
|
610
|
+
a color, all glyphs are colored with that color. Default is
|
|
611
|
+
colorcet.b_glasbey_category10 from the colorcet package.
|
|
612
|
+
order : list or None
|
|
613
|
+
If not None, must be a list of unique group names when the input
|
|
614
|
+
data frame is grouped by `cats`. The order of the list specifies
|
|
615
|
+
the ordering of the categorical variables in the legend. If
|
|
616
|
+
None, the categories appear in the order in which they appeared
|
|
617
|
+
in the inputted data frame.
|
|
618
|
+
p : bokeh.plotting.Figure instance, or None (default)
|
|
619
|
+
If None, create a new figure. Otherwise, populate the existing
|
|
620
|
+
figure `p`.
|
|
621
|
+
rug : bool, default True
|
|
622
|
+
If True, also include a rug plot. If, however, `bins` is 'exact'
|
|
623
|
+
or 'integer', the `rug` kwarg is ignored.
|
|
624
|
+
rug_height : float, default None
|
|
625
|
+
Height of the rug plot as a fraction of the highest point in the
|
|
626
|
+
histograms. For 'overlay' arrangement, default is 0.05. For
|
|
627
|
+
'stacked' arrangement, default is 0.2.
|
|
628
|
+
show_legend : bool, default False
|
|
629
|
+
If True, display legend.
|
|
630
|
+
legend_label : str, default None
|
|
631
|
+
If `cats` is None and `show_legend` is True, then if
|
|
632
|
+
`legend_label` is not None, a legend is created for the glyph
|
|
633
|
+
on the plot and labeled with `legend_label`. Otherwise, no
|
|
634
|
+
legend is created if `cats` is None.
|
|
635
|
+
legend_location : str, default 'right'
|
|
636
|
+
Location of legend. If one of "right", "left", "above", or
|
|
637
|
+
"below", the legend is placed outside of the plot area. If one
|
|
638
|
+
of "top_left", "top_center", "top_right", "center_right",
|
|
639
|
+
"bottom_right", "bottom_center", "bottom_left", "center_left",
|
|
640
|
+
or "center", the legend is placed within the plot area. If a
|
|
641
|
+
2-tuple, legend is placed according to the coordinates in the
|
|
642
|
+
tuple.
|
|
643
|
+
legend_orientation : str, default 'vertical'
|
|
644
|
+
Either 'horizontal' or 'vertical'.
|
|
645
|
+
legend_click_policy : str, default 'hide'
|
|
646
|
+
Either 'hide', 'mute', or None; how the glyphs respond when the
|
|
647
|
+
corresponding category is clicked in the legend.
|
|
648
|
+
tooltips : list of 2-tuples
|
|
649
|
+
Specification for tooltips as per Bokeh specifications. For
|
|
650
|
+
example, if we want `col1` and `col2` tooltips, we can use
|
|
651
|
+
`tooltips=[('label 1', '@col1'), ('label 2', '@col2')]`. Ignored
|
|
652
|
+
if `rug` is False.
|
|
653
|
+
bins : int, array_like, or str, default 'freedman-diaconis'
|
|
654
|
+
If int or array_like, setting for `bins` kwarg to be passed to
|
|
655
|
+
`np.histogram()`. If 'exact', then each unique value in the
|
|
656
|
+
data gets its own bin. If 'integer', then integer data is
|
|
657
|
+
assumed and each integer gets its own bin. If 'sqrt', uses the
|
|
658
|
+
square root rule to determine number of bins. If
|
|
659
|
+
`freedman-diaconis`, uses the Freedman-Diaconis rule for number
|
|
660
|
+
of bins.
|
|
661
|
+
density : bool, default False
|
|
662
|
+
If True, normalize the histograms. Otherwise, base the
|
|
663
|
+
histograms on counts.
|
|
664
|
+
style : None or one of ['step', 'step_filled']
|
|
665
|
+
Default for overlaid histograms is 'step' and for stacked
|
|
666
|
+
histograms 'step_filled'. The exception is when `conf_int` is
|
|
667
|
+
True, in which case `style` must be 'step'.
|
|
668
|
+
arrangement : 'stack' or 'overlay'
|
|
669
|
+
Arrangement of histograms. If 'overlay', histograms are overlaid
|
|
670
|
+
on the same plot. If 'stack', histograms are stacked one on top
|
|
671
|
+
of the other. By default, if `cats` is None, `arrangement` is
|
|
672
|
+
'overlay', and otherwise `arrangement` is 'stack'.
|
|
673
|
+
mirror : bool, default False
|
|
674
|
+
If True, reflect the histogram through zero. Ignored if
|
|
675
|
+
`arrangement == 'overlay'`.
|
|
676
|
+
hist_height : float, default 0.75
|
|
677
|
+
Maximal height of histogram or its confidence interval as a
|
|
678
|
+
fraction of available height along categorical axis. Only active
|
|
679
|
+
when `arrangement` is 'stack'.
|
|
680
|
+
conf_int : bool, default False
|
|
681
|
+
If True, display confidence interval of histogram.
|
|
682
|
+
ptiles : list, default [2.5, 97.5]
|
|
683
|
+
The percentiles to use for the confidence interval. Ignored if
|
|
684
|
+
`conf_int` is False.
|
|
685
|
+
n_bs_reps : int, default 10,000
|
|
686
|
+
Number of bootstrap replicates to do to compute confidence
|
|
687
|
+
interval. Ignored if `conf_int` is False.
|
|
688
|
+
line_kwargs : dict
|
|
689
|
+
Keyword arguments to pass to `p.line()` in constructing the
|
|
690
|
+
histograms. By default, {"line_width": 2}.
|
|
691
|
+
fill_kwargs : dict
|
|
692
|
+
Keyword arguments to pass to `p.patch()` when making the fill
|
|
693
|
+
for the step-filled histogram or confidence intervals. Ignored
|
|
694
|
+
if `style = 'step'` and `conf_int` is False. By default
|
|
695
|
+
{"fill_alpha": 0.3, "line_alpha": 0}.
|
|
696
|
+
binsep_kwargs : dict, default {'line_alpha': 0}
|
|
697
|
+
Kwargs passed to p.line for lines separating bins in the
|
|
698
|
+
histogram. By default, the lines separating bins are not
|
|
699
|
+
visible.
|
|
700
|
+
rug_kwargs : dict
|
|
701
|
+
Keyword arguments to pass to `p.multi_line()` when making the
|
|
702
|
+
rug plot.
|
|
703
|
+
horizontal : bool or None, default None
|
|
704
|
+
Deprecated. Use `q_axis`.
|
|
705
|
+
val : hashable
|
|
706
|
+
Deprecated, use `q`.
|
|
707
|
+
click_policy : str, default 'hide'
|
|
708
|
+
Deprecated. Use `legend_click_policy`.
|
|
709
|
+
conf_int_kwargs : dict
|
|
710
|
+
Deprecated. Use `fill_kwargs`.
|
|
711
|
+
kind : str, default 'step_filled'
|
|
712
|
+
Deprecated. Use `style`.
|
|
713
|
+
kwargs
|
|
714
|
+
Any kwargs to be passed to `bokeh.plotting.figure()` when making
|
|
715
|
+
the plot.
|
|
716
|
+
|
|
717
|
+
Returns
|
|
718
|
+
-------
|
|
719
|
+
output : Bokeh figure
|
|
720
|
+
Figure populated with histograms.
|
|
721
|
+
|
|
722
|
+
Notes
|
|
723
|
+
-----
|
|
724
|
+
.. Confidence intervals for the histogram are computed using
|
|
725
|
+
nonparametric bootstrap as follows. The bins are established as
|
|
726
|
+
per user input via the `bins` kwarg. These bins are fixed for all
|
|
727
|
+
bootstrap replicates. Then, for each bootstrap sample drawn, the
|
|
728
|
+
histogram is computed for the bins. The confidence interval is
|
|
729
|
+
then computed from these bootstrap samples.
|
|
730
|
+
"""
|
|
731
|
+
# Protect against mutability of dicts
|
|
732
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
733
|
+
fill_kwargs = copy.copy(fill_kwargs)
|
|
734
|
+
rug_kwargs = copy.copy(rug_kwargs)
|
|
735
|
+
binsep_kwargs = copy.copy(binsep_kwargs)
|
|
736
|
+
|
|
737
|
+
if conf_int:
|
|
738
|
+
if not isinstance(ptiles, (list, tuple, np.ndarray)) or len(ptiles) != 2:
|
|
739
|
+
raise RuntimeError("`ptiles` must be a list or tuple of length 2.")
|
|
740
|
+
else:
|
|
741
|
+
ptiles = np.sort(ptiles)
|
|
742
|
+
|
|
743
|
+
# Check the deprecation of `kind` kwarg (not in utils._parse_deprecations because
|
|
744
|
+
# `kind` is a valid kwarg for ECDFs)
|
|
745
|
+
if kind is not None:
|
|
746
|
+
if style != kind:
|
|
747
|
+
raise RuntimeError(
|
|
748
|
+
"`kind` and `style` in disagreement. Use `style`; `kind` is deprecated."
|
|
749
|
+
)
|
|
750
|
+
warnings.warn(
|
|
751
|
+
f"`kind` is deprecated. Use `style`. Using style='{style}'.",
|
|
752
|
+
DeprecationWarning,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if arrangement is None:
|
|
756
|
+
arrangement = "overlay" if cats is None else "stack"
|
|
757
|
+
|
|
758
|
+
if style is None:
|
|
759
|
+
if conf_int:
|
|
760
|
+
style = "step"
|
|
761
|
+
else:
|
|
762
|
+
if arrangement == "stack":
|
|
763
|
+
style = "step_filled"
|
|
764
|
+
else:
|
|
765
|
+
style = "step"
|
|
766
|
+
|
|
767
|
+
# Check style
|
|
768
|
+
if style not in ["step", "step_filled"]:
|
|
769
|
+
raise RuntimeError("`style` must be one of 'step', 'step_filled'.")
|
|
770
|
+
|
|
771
|
+
if arrangement == "stack":
|
|
772
|
+
if rug_height is None:
|
|
773
|
+
rug_height = 0.2
|
|
774
|
+
elif arrangement != "overlay":
|
|
775
|
+
raise RuntimeError(
|
|
776
|
+
"Only allowed values for `arrangement` are 'stack' and 'overlay'."
|
|
777
|
+
)
|
|
778
|
+
elif rug_height is None:
|
|
779
|
+
rug_height = 0.05
|
|
780
|
+
|
|
781
|
+
if style == "step_filled" and conf_int:
|
|
782
|
+
raise RuntimeError(
|
|
783
|
+
"`style` must be 'step' when confidence intervals are displayed."
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
if conf_int and style == "step_filled":
|
|
787
|
+
raise RuntimeError(
|
|
788
|
+
f"`style` must be 'step' when confidence intervals are included."
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
q, legend_click_policy, fill_kwargs = utils._parse_deprecations(
|
|
792
|
+
q,
|
|
793
|
+
q_axis,
|
|
794
|
+
val,
|
|
795
|
+
horizontal,
|
|
796
|
+
"y",
|
|
797
|
+
click_policy,
|
|
798
|
+
legend_click_policy,
|
|
799
|
+
conf_int_kwargs,
|
|
800
|
+
fill_kwargs,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
if isinstance(bins, str) and bins in ["integer", "exact"]:
|
|
804
|
+
if rug is None:
|
|
805
|
+
rug = False
|
|
806
|
+
|
|
807
|
+
if rug:
|
|
808
|
+
warnings.warn("Rug plot not generated for integer or exact bins.")
|
|
809
|
+
rug = False
|
|
810
|
+
elif rug is None:
|
|
811
|
+
rug = True
|
|
812
|
+
|
|
813
|
+
if palette is None:
|
|
814
|
+
palette = colorcet.b_glasbey_category10
|
|
815
|
+
elif isinstance(palette, str):
|
|
816
|
+
palette = [palette]
|
|
817
|
+
|
|
818
|
+
utils._check_cats_none(cats, order, show_legend, legend_label)
|
|
819
|
+
|
|
820
|
+
df, q, cats, show_legend = utils._data_cats(
|
|
821
|
+
data, q, cats, show_legend, legend_label
|
|
822
|
+
)
|
|
823
|
+
order = utils._order_to_str(order)
|
|
824
|
+
|
|
825
|
+
if arrangement == "stack":
|
|
826
|
+
if show_legend is None:
|
|
827
|
+
show_legend = False
|
|
828
|
+
|
|
829
|
+
if show_legend:
|
|
830
|
+
warnings.warn(
|
|
831
|
+
"Cannot show legend with arrangement='stack'. There is no legend to show."
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
if show_legend is None:
|
|
835
|
+
show_legend = True
|
|
836
|
+
|
|
837
|
+
if isinstance(bins, str) and bins not in [
|
|
838
|
+
"integer",
|
|
839
|
+
"exact",
|
|
840
|
+
"sqrt",
|
|
841
|
+
"freedman-diaconis",
|
|
842
|
+
]:
|
|
843
|
+
raise RuntimeError("Invalid bin specification.")
|
|
844
|
+
|
|
845
|
+
cats, cols = utils._check_cat_input(
|
|
846
|
+
df, cats, q, None, None, None, palette, order, kwargs
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
# Defaults for histogram
|
|
850
|
+
if line_kwargs is None:
|
|
851
|
+
line_kwargs = {"line_width": 2}
|
|
852
|
+
if fill_kwargs is None:
|
|
853
|
+
fill_kwargs = {}
|
|
854
|
+
if binsep_kwargs is None:
|
|
855
|
+
binsep_kwargs = {"line_alpha": 0.0}
|
|
856
|
+
elif "alpha" not in binsep_kwargs and "line_alpha" not in binsep_kwargs:
|
|
857
|
+
binsep_kwargs["line_alpha"] = 1.0
|
|
858
|
+
if "line_width" not in binsep_kwargs and "line_width" in line_kwargs:
|
|
859
|
+
binsep_kwargs["line_width"] = line_kwargs["line_width"]
|
|
860
|
+
if "alpha" not in fill_kwargs and "fill_alpha" not in fill_kwargs:
|
|
861
|
+
fill_kwargs["fill_alpha"] = 0.3
|
|
862
|
+
if (
|
|
863
|
+
"alpha" not in fill_kwargs
|
|
864
|
+
and "line_alpha" not in fill_kwargs
|
|
865
|
+
and "line_color" not in fill_kwargs
|
|
866
|
+
):
|
|
867
|
+
fill_kwargs["line_alpha"] = 0
|
|
868
|
+
binsep_color_supplied = "color" in binsep_kwargs or "line_color" in binsep_kwargs
|
|
869
|
+
|
|
870
|
+
# Defaults for rug_kwargs
|
|
871
|
+
if rug_kwargs is None:
|
|
872
|
+
rug_kwargs = dict(line_alpha=0.5, line_width=0.5)
|
|
873
|
+
elif not isinstance(rug_kwargs, dict):
|
|
874
|
+
raise RuntimeError("`rug_kwargs` must be a dictionary.")
|
|
875
|
+
if "alpha" not in rug_kwargs and "line_alpha" not in rug_kwargs:
|
|
876
|
+
rug_kwargs["line_alpha"] = 0.5
|
|
877
|
+
if "line_width" not in rug_kwargs:
|
|
878
|
+
rug_kwargs["line_width"] = 0.5
|
|
879
|
+
if "name" not in rug_kwargs:
|
|
880
|
+
rug_kwargs["name"] = "hover_glyphs"
|
|
881
|
+
|
|
882
|
+
# Change any kwarg of "color" to line_color and fill_color, same with alpha
|
|
883
|
+
fill_kwargs = utils._specific_fill_and_color_kwargs(fill_kwargs, "fill")
|
|
884
|
+
line_kwargs = utils._specific_fill_and_color_kwargs(line_kwargs, "line")
|
|
885
|
+
rug_kwargs = utils._specific_fill_and_color_kwargs(rug_kwargs, "line")
|
|
886
|
+
binsep_kwargs = utils._specific_fill_and_color_kwargs(binsep_kwargs, "line")
|
|
887
|
+
|
|
888
|
+
_, df["__label"] = utils._source_and_labels_from_cats(df, cats)
|
|
889
|
+
cols += ["__label"]
|
|
890
|
+
|
|
891
|
+
df = _sort_df(df, cats, order)
|
|
892
|
+
|
|
893
|
+
if isinstance(bins, str) and bins == "exact":
|
|
894
|
+
a = np.unique(df[q])
|
|
895
|
+
if len(a) == 1:
|
|
896
|
+
bins = np.array([a[0] - 0.5, a[0] + 0.5])
|
|
897
|
+
else:
|
|
898
|
+
bins = np.concatenate(
|
|
899
|
+
(
|
|
900
|
+
(a[0] - (a[1] - a[0]) / 2,),
|
|
901
|
+
(a[1:] + a[:-1]) / 2,
|
|
902
|
+
(a[-1] + (a[-1] - a[-2]) / 2,),
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
elif isinstance(bins, str) and bins == "integer":
|
|
906
|
+
if np.any(df[q] != np.round(df[q])):
|
|
907
|
+
raise RuntimeError("'integer' bins chosen, but data are not integer.")
|
|
908
|
+
bins = np.arange(df[q].min() - 1, df[q].max() + 1) + 0.5
|
|
909
|
+
|
|
910
|
+
if arrangement == "stack":
|
|
911
|
+
if cats is not None:
|
|
912
|
+
grouped = df.groupby(cats, sort=False)
|
|
913
|
+
return _stacked_histograms(
|
|
914
|
+
df,
|
|
915
|
+
grouped,
|
|
916
|
+
q,
|
|
917
|
+
bins,
|
|
918
|
+
density,
|
|
919
|
+
palette,
|
|
920
|
+
q_axis,
|
|
921
|
+
order,
|
|
922
|
+
p,
|
|
923
|
+
mirror,
|
|
924
|
+
hist_height,
|
|
925
|
+
style,
|
|
926
|
+
conf_int,
|
|
927
|
+
ptiles,
|
|
928
|
+
n_bs_reps,
|
|
929
|
+
rug,
|
|
930
|
+
rug_height,
|
|
931
|
+
tooltips,
|
|
932
|
+
line_kwargs,
|
|
933
|
+
fill_kwargs,
|
|
934
|
+
binsep_kwargs,
|
|
935
|
+
rug_kwargs,
|
|
936
|
+
kwargs,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
if p is None:
|
|
940
|
+
kwargs = utils._fig_dimensions(kwargs)
|
|
941
|
+
|
|
942
|
+
if "x_axis_label" not in kwargs:
|
|
943
|
+
if q_axis == "y":
|
|
944
|
+
if density:
|
|
945
|
+
kwargs["x_axis_label"] = "density"
|
|
946
|
+
else:
|
|
947
|
+
kwargs["x_axis_label"] = "count"
|
|
948
|
+
else:
|
|
949
|
+
kwargs["x_axis_label"] = q
|
|
950
|
+
|
|
951
|
+
if "y_axis_label" not in kwargs:
|
|
952
|
+
if q_axis == "y":
|
|
953
|
+
kwargs["y_axis_label"] = q
|
|
954
|
+
else:
|
|
955
|
+
if density:
|
|
956
|
+
kwargs["y_axis_label"] = "density"
|
|
957
|
+
else:
|
|
958
|
+
kwargs["y_axis_label"] = "count"
|
|
959
|
+
|
|
960
|
+
if q_axis == "y":
|
|
961
|
+
if "x_range" not in kwargs:
|
|
962
|
+
kwargs["x_range"] = bokeh.models.DataRange1d(start=0)
|
|
963
|
+
else:
|
|
964
|
+
if "y_range" not in kwargs:
|
|
965
|
+
kwargs["y_range"] = bokeh.models.DataRange1d(start=0)
|
|
966
|
+
|
|
967
|
+
p = bokeh.plotting.figure(**kwargs)
|
|
968
|
+
|
|
969
|
+
# Integer ticks of histogram of counts
|
|
970
|
+
if not density:
|
|
971
|
+
if q_axis == "x":
|
|
972
|
+
p.yaxis.ticker = bokeh.models.BasicTicker(
|
|
973
|
+
min_interval=1, num_minor_ticks=0
|
|
974
|
+
)
|
|
975
|
+
else:
|
|
976
|
+
p.xaxis.ticker = bokeh.models.BasicTicker(
|
|
977
|
+
min_interval=1, num_minor_ticks=0
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# Explicitly loop to enable click policies on the legend (not possible with factors)
|
|
981
|
+
max_height = 0
|
|
982
|
+
lines = []
|
|
983
|
+
labels = []
|
|
984
|
+
patches = []
|
|
985
|
+
binsep_lines = []
|
|
986
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
987
|
+
numerical_bins, e, f = _compute_histogram(g[q], bins, density)
|
|
988
|
+
e0, f0 = _hist_for_plotting(e, f)
|
|
989
|
+
|
|
990
|
+
if conf_int:
|
|
991
|
+
fill_kwargs["fill_color"] = palette[i % len(palette)]
|
|
992
|
+
|
|
993
|
+
q_vals = g[q].values
|
|
994
|
+
f_reps = np.empty((n_bs_reps, len(f)))
|
|
995
|
+
for j in range(n_bs_reps):
|
|
996
|
+
_, _, f_reps[j, :] = _compute_histogram(
|
|
997
|
+
np.random.choice(q_vals, len(q_vals)), numerical_bins, density
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
f_ptiles = np.percentile(f_reps, ptiles, axis=0)
|
|
1001
|
+
|
|
1002
|
+
_, f0_low = _hist_for_plotting(e, f_ptiles[0, :])
|
|
1003
|
+
_, f0_high = _hist_for_plotting(e, f_ptiles[1, :])
|
|
1004
|
+
if q_axis == "y":
|
|
1005
|
+
p, patch = utils._fill_between(
|
|
1006
|
+
p, f0_low, e0, f0_high, e0, **fill_kwargs
|
|
1007
|
+
)
|
|
1008
|
+
else:
|
|
1009
|
+
p, patch = utils._fill_between(
|
|
1010
|
+
p, e0, f0_low, e0, f0_high, **fill_kwargs
|
|
1011
|
+
)
|
|
1012
|
+
patches.append(patch)
|
|
1013
|
+
|
|
1014
|
+
max_height = max(f0.max(), max_height)
|
|
1015
|
+
|
|
1016
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
1017
|
+
if not binsep_color_supplied:
|
|
1018
|
+
binsep_kwargs["color"] = palette[i % len(palette)]
|
|
1019
|
+
|
|
1020
|
+
if q_axis == "y":
|
|
1021
|
+
lines.append(p.line(f0, e0, **line_kwargs))
|
|
1022
|
+
else:
|
|
1023
|
+
lines.append(p.line(e0, f0, **line_kwargs))
|
|
1024
|
+
|
|
1025
|
+
labels.append(g["__label"].iloc[0])
|
|
1026
|
+
|
|
1027
|
+
if style == "step_filled":
|
|
1028
|
+
x2 = [e0.min(), e0.max()]
|
|
1029
|
+
y2 = [0, 0]
|
|
1030
|
+
fill_kwargs["color"] = palette[i % len(palette)]
|
|
1031
|
+
if q_axis == "y":
|
|
1032
|
+
p, patch = utils._fill_between(p, f0, e0, y2, x2, **fill_kwargs)
|
|
1033
|
+
else:
|
|
1034
|
+
p, patch = utils._fill_between(p, e0, f0, x2, y2, **fill_kwargs)
|
|
1035
|
+
patches.append(patch)
|
|
1036
|
+
|
|
1037
|
+
if binsep_kwargs["line_alpha"] > 0:
|
|
1038
|
+
heights = [min(f0[j], f0[j + 1]) for j in range(2, len(f0) - 2, 2)]
|
|
1039
|
+
binsep_height = [[0, height] for height in heights]
|
|
1040
|
+
binsep_q = [[edge, edge] for edge in e0[2:-2:2]]
|
|
1041
|
+
|
|
1042
|
+
if q_axis == "y":
|
|
1043
|
+
binsep_lines.append(
|
|
1044
|
+
p.multi_line(binsep_height, binsep_q, **binsep_kwargs)
|
|
1045
|
+
)
|
|
1046
|
+
else:
|
|
1047
|
+
binsep_lines.append(
|
|
1048
|
+
p.multi_line(binsep_q, binsep_height, **binsep_kwargs)
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
# Put in the rug plot
|
|
1052
|
+
if rug:
|
|
1053
|
+
y = [0, max_height * rug_height]
|
|
1054
|
+
|
|
1055
|
+
for i, (_, g) in enumerate(df.groupby(cats, sort=False)):
|
|
1056
|
+
xs = [[q_val, q_val] for q_val in g[q].values]
|
|
1057
|
+
ys = [y] * len(g)
|
|
1058
|
+
|
|
1059
|
+
if q_axis == "y":
|
|
1060
|
+
xs, ys = ys, xs
|
|
1061
|
+
|
|
1062
|
+
cds = bokeh.models.ColumnDataSource(g)
|
|
1063
|
+
cds.data["__xs"] = xs
|
|
1064
|
+
cds.data["__ys"] = ys
|
|
1065
|
+
|
|
1066
|
+
if "color" not in rug_kwargs and "line_color" not in rug_kwargs:
|
|
1067
|
+
p.multi_line(
|
|
1068
|
+
source=cds,
|
|
1069
|
+
xs="__xs",
|
|
1070
|
+
ys="__ys",
|
|
1071
|
+
line_color=palette[i % len(palette)],
|
|
1072
|
+
**rug_kwargs,
|
|
1073
|
+
)
|
|
1074
|
+
else:
|
|
1075
|
+
p.multi_line(source=cds, xs="__xs", ys="__ys", **rug_kwargs)
|
|
1076
|
+
|
|
1077
|
+
if tooltips is not None:
|
|
1078
|
+
p.add_tools(bokeh.models.HoverTool(tooltips=tooltips, name="hover_glyphs"))
|
|
1079
|
+
|
|
1080
|
+
return _dist_legend(
|
|
1081
|
+
p,
|
|
1082
|
+
show_legend,
|
|
1083
|
+
legend_location,
|
|
1084
|
+
legend_orientation,
|
|
1085
|
+
legend_click_policy,
|
|
1086
|
+
labels,
|
|
1087
|
+
[],
|
|
1088
|
+
lines,
|
|
1089
|
+
patches,
|
|
1090
|
+
[],
|
|
1091
|
+
[],
|
|
1092
|
+
[],
|
|
1093
|
+
[],
|
|
1094
|
+
[],
|
|
1095
|
+
extra_glyphs=binsep_lines,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def spike(
|
|
1100
|
+
data=None,
|
|
1101
|
+
q=None,
|
|
1102
|
+
cats=None,
|
|
1103
|
+
palette=None,
|
|
1104
|
+
order=None,
|
|
1105
|
+
q_axis="x",
|
|
1106
|
+
p=None,
|
|
1107
|
+
show_legend=None,
|
|
1108
|
+
legend_label=None,
|
|
1109
|
+
legend_location="right",
|
|
1110
|
+
legend_orientation="vertical",
|
|
1111
|
+
legend_click_policy="hide",
|
|
1112
|
+
fraction=False,
|
|
1113
|
+
style=None,
|
|
1114
|
+
arrangement=None,
|
|
1115
|
+
spike_height=0.75,
|
|
1116
|
+
conf_int=False,
|
|
1117
|
+
ptiles=(2.5, 97.5),
|
|
1118
|
+
n_bs_reps=10000,
|
|
1119
|
+
line_kwargs=None,
|
|
1120
|
+
marker_kwargs=None,
|
|
1121
|
+
horizontal=None,
|
|
1122
|
+
val=None,
|
|
1123
|
+
click_policy=None,
|
|
1124
|
+
density=None,
|
|
1125
|
+
**kwargs,
|
|
1126
|
+
):
|
|
1127
|
+
"""
|
|
1128
|
+
Make a spike plot.
|
|
1129
|
+
|
|
1130
|
+
Parameters
|
|
1131
|
+
----------
|
|
1132
|
+
data : Pandas DataFrame, 1D Numpy array, or xarray
|
|
1133
|
+
DataFrame containing tidy data for plotting. If a Numpy array,
|
|
1134
|
+
a single category is assumed and a spike plot generated from
|
|
1135
|
+
data.
|
|
1136
|
+
q : hashable
|
|
1137
|
+
Name of column to use as quantitative variable if `data` is a
|
|
1138
|
+
Pandas DataFrame. Otherwise, `q` is used as the quantitative
|
|
1139
|
+
axis label.
|
|
1140
|
+
cats : hashable or list of hashables
|
|
1141
|
+
Name of column(s) to use as categorical variable(s).
|
|
1142
|
+
q_axis : str, either 'x' or 'y', default 'x'
|
|
1143
|
+
Axis along which the quantitative value varies.
|
|
1144
|
+
palette : list colors, or single color string
|
|
1145
|
+
If a list, color palette to use. If a single string representing
|
|
1146
|
+
a color, all glyphs are colored with that color. Default is
|
|
1147
|
+
colorcet.b_glasbey_category10 from the colorcet package.
|
|
1148
|
+
order : list or None
|
|
1149
|
+
If not None, must be a list of unique group names when the input
|
|
1150
|
+
data frame is grouped by `cats`. The order of the list specifies
|
|
1151
|
+
the ordering of the categorical variables in the legend. If
|
|
1152
|
+
None, the categories appear in the order in which they appeared
|
|
1153
|
+
in the inputted data frame.
|
|
1154
|
+
p : bokeh.plotting.Figure instance, or None (default)
|
|
1155
|
+
If None, create a new figure. Otherwise, populate the existing
|
|
1156
|
+
figure `p`.
|
|
1157
|
+
show_legend : bool, default False
|
|
1158
|
+
If True, display legend.
|
|
1159
|
+
legend_label : str, default None
|
|
1160
|
+
If `cats` is None and `show_legend` is True, then if
|
|
1161
|
+
`legend_label` is not None, a legend is created for the glyph
|
|
1162
|
+
on the plot and labeled with `legend_label`. Otherwise, no
|
|
1163
|
+
legend is created if `cats` is None.
|
|
1164
|
+
legend_location : str, default 'right'
|
|
1165
|
+
Location of legend. If one of "right", "left", "above", or
|
|
1166
|
+
"below", the legend is placed outside of the plot area. If one
|
|
1167
|
+
of "top_left", "top_center", "top_right", "center_right",
|
|
1168
|
+
"bottom_right", "bottom_center", "bottom_left", "center_left",
|
|
1169
|
+
or "center", the legend is placed within the plot area. If a
|
|
1170
|
+
2-tuple, legend is placed according to the coordinates in the
|
|
1171
|
+
tuple.
|
|
1172
|
+
legend_orientation : str, default 'vertical'
|
|
1173
|
+
Either 'horizontal' or 'vertical'.
|
|
1174
|
+
legend_click_policy : str, default 'hide'
|
|
1175
|
+
Either 'hide', 'mute', or None; how the glyphs respond when the
|
|
1176
|
+
corresponding category is clicked in the legend.
|
|
1177
|
+
fraction : bool, default False
|
|
1178
|
+
If True, the spike height is given by the fraction of data
|
|
1179
|
+
points having the given value. Otherwise, the height of a spike
|
|
1180
|
+
is given by the count of data points having the given value.
|
|
1181
|
+
style : None or one of ['spike', 'spike-dot', 'dot']
|
|
1182
|
+
'spike' gives a traditional spike plot. 'spike-dot' additionally
|
|
1183
|
+
features dots on top of the spikes, similar in appearance to a
|
|
1184
|
+
lollipop plot. 'dot' has the dot at the top of the spike, but
|
|
1185
|
+
the spike is not shown. Default is 'spike-dot', unless
|
|
1186
|
+
`conf_int` is True and the number of categorical values is
|
|
1187
|
+
greater than one, in which case the default is 'dot' (and only
|
|
1188
|
+
'dot' is allowed) for confidence intervals to avoid clashes.
|
|
1189
|
+
arrangement : 'stack' or 'overlay', default 'stack'
|
|
1190
|
+
Arrangement of spike plots. If 'overlay', spikes are overlaid
|
|
1191
|
+
on the same plot. If 'stack', spikes are stacked one on top
|
|
1192
|
+
of the other.
|
|
1193
|
+
spike_height : float, default 0.75
|
|
1194
|
+
Maximal height of spike or its confidence interval as a
|
|
1195
|
+
fraction of available height along categorical axis. Only active
|
|
1196
|
+
when `arrangement` is 'stack'.
|
|
1197
|
+
conf_int : bool, default False
|
|
1198
|
+
If True, display confidence interval of the spikes.
|
|
1199
|
+
ptiles : list with two elements, default [2.5, 97.5]
|
|
1200
|
+
The percentiles to use for the confidence interval. Ignored if
|
|
1201
|
+
`conf_int` is False.
|
|
1202
|
+
n_bs_reps : int, default 10,000
|
|
1203
|
+
Number of bootstrap replicates to do to compute confidence
|
|
1204
|
+
interval. Ignored if `conf_int` is False.
|
|
1205
|
+
marker_kwargs : dict
|
|
1206
|
+
Keyword arguments to be passed to `p.scatter()` for dots at the
|
|
1207
|
+
top of spikes.
|
|
1208
|
+
line_kwargs : dict
|
|
1209
|
+
Keyword arguments to pass to `p.segment()` in constructing the
|
|
1210
|
+
spikes and confidence intervals. By default, {"line_width": 2}.
|
|
1211
|
+
horizontal : bool or None, default None
|
|
1212
|
+
Deprecated. Use `q_axis`.
|
|
1213
|
+
val : hashable
|
|
1214
|
+
Deprecated, use `q`.
|
|
1215
|
+
click_policy : str, default 'hide'
|
|
1216
|
+
Deprecated. Use `legend_click_policy`.
|
|
1217
|
+
density : bool, default None
|
|
1218
|
+
Deprecated. Use `fraction`.
|
|
1219
|
+
kwargs
|
|
1220
|
+
Any kwargs to be passed to `bokeh.plotting.figure()` when making
|
|
1221
|
+
the plot.
|
|
1222
|
+
|
|
1223
|
+
Returns
|
|
1224
|
+
-------
|
|
1225
|
+
output : Bokeh figure
|
|
1226
|
+
Figure populated with spikes.
|
|
1227
|
+
|
|
1228
|
+
Notes
|
|
1229
|
+
-----
|
|
1230
|
+
.. Confidence intervals for the spikes are computed using
|
|
1231
|
+
nonparametric bootstrap. For each bootstrap sample drawn, the
|
|
1232
|
+
number of data points having each unique value is counted. The
|
|
1233
|
+
confidence interval is then computed from these bootstrap
|
|
1234
|
+
samples.
|
|
1235
|
+
"""
|
|
1236
|
+
# Protect against mutability of dicts
|
|
1237
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
1238
|
+
marker_kwargs = copy.copy(marker_kwargs)
|
|
1239
|
+
|
|
1240
|
+
if conf_int:
|
|
1241
|
+
if not isinstance(ptiles, (list, tuple, np.ndarray)) or len(ptiles) != 2:
|
|
1242
|
+
raise RuntimeError("`ptiles` must be a list or tuple of length 2.")
|
|
1243
|
+
else:
|
|
1244
|
+
ptiles = np.sort(ptiles)
|
|
1245
|
+
|
|
1246
|
+
# Quick check to see if there is only one category.
|
|
1247
|
+
# This is not an ideal way to do it, since we convert to Pandas df
|
|
1248
|
+
# just for the check, which we do later as well
|
|
1249
|
+
data_check, _, _, _ = utils._data_cats(data, q, cats, False, None)
|
|
1250
|
+
one_cat = cats is None or len(data_check.groupby(cats)) == 1
|
|
1251
|
+
|
|
1252
|
+
if arrangement is None:
|
|
1253
|
+
arrangement = "overlay" if one_cat else "stack"
|
|
1254
|
+
|
|
1255
|
+
# Use fraction, not density
|
|
1256
|
+
if density is not None:
|
|
1257
|
+
raise RuntimeError("For spike plots, use `fraction`, not `density`.")
|
|
1258
|
+
|
|
1259
|
+
if style is None:
|
|
1260
|
+
if conf_int or (arrangement == "overlay" and not one_cat):
|
|
1261
|
+
style = "dot"
|
|
1262
|
+
else:
|
|
1263
|
+
style = "spike-dot"
|
|
1264
|
+
|
|
1265
|
+
if style not in ["dot", "spike", "spike-dot"]:
|
|
1266
|
+
raise RuntimeError(
|
|
1267
|
+
"Valid values for `style` kwarg are 'dot', 'spike', and 'spike-dot'."
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
if arrangement not in ["stack", "overlay"]:
|
|
1271
|
+
raise RuntimeError(
|
|
1272
|
+
"Only allowed values for `arrangement` are 'stack' and 'overlay'."
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
if "spike" in style and conf_int:
|
|
1276
|
+
raise RuntimeError(
|
|
1277
|
+
"`style` must be 'dot' when confidence intervals are displayed."
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
if arrangement == "overlay" and cats is not None and "dot" not in style:
|
|
1281
|
+
raise RuntimeError(
|
|
1282
|
+
"`style` must be 'dot' or 'spike-dot' for overlay arrangement with more than one category."
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
q, legend_click_policy, _ = utils._parse_deprecations(
|
|
1286
|
+
q,
|
|
1287
|
+
q_axis,
|
|
1288
|
+
val,
|
|
1289
|
+
horizontal,
|
|
1290
|
+
"y",
|
|
1291
|
+
click_policy,
|
|
1292
|
+
legend_click_policy,
|
|
1293
|
+
None,
|
|
1294
|
+
{},
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
# Can't have `q` be 'count' in Pandas v. 2.x. Just make it always illegal
|
|
1298
|
+
if q == "count":
|
|
1299
|
+
raise RuntimeError(
|
|
1300
|
+
'Cannot make a spike plot with a quantitative variable named "count." '
|
|
1301
|
+
+ 'Rename the "count" column and start again.'
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
if palette is None:
|
|
1305
|
+
palette = colorcet.b_glasbey_category10
|
|
1306
|
+
elif isinstance(palette, str):
|
|
1307
|
+
palette = [palette]
|
|
1308
|
+
|
|
1309
|
+
utils._check_cats_none(cats, order, show_legend, legend_label)
|
|
1310
|
+
|
|
1311
|
+
df, q, cats, show_legend = utils._data_cats(
|
|
1312
|
+
data, q, cats, show_legend, legend_label
|
|
1313
|
+
)
|
|
1314
|
+
order = utils._order_to_str(order)
|
|
1315
|
+
|
|
1316
|
+
if arrangement == "stack":
|
|
1317
|
+
if show_legend is None:
|
|
1318
|
+
show_legend = False
|
|
1319
|
+
|
|
1320
|
+
if show_legend:
|
|
1321
|
+
warnings.warn(
|
|
1322
|
+
"Cannot show legend with arrangement='stack'. There is no legend to show."
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
if show_legend is None:
|
|
1326
|
+
show_legend = True
|
|
1327
|
+
|
|
1328
|
+
cats, cols = utils._check_cat_input(
|
|
1329
|
+
df, cats, q, None, None, None, palette, order, kwargs
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
# Defaults for spikes
|
|
1333
|
+
if line_kwargs is None:
|
|
1334
|
+
line_kwargs = {"line_width": 2}
|
|
1335
|
+
|
|
1336
|
+
# Marker kwargs use Bokeh defaults
|
|
1337
|
+
marker_kwargs = utils._check_marker_kwargs(marker_kwargs)
|
|
1338
|
+
|
|
1339
|
+
# Change any kwarg of "color" to line_color and fill_color, same with alpha
|
|
1340
|
+
marker_kwargs = utils._specific_fill_and_color_kwargs(marker_kwargs, "marker")
|
|
1341
|
+
line_kwargs = utils._specific_fill_and_color_kwargs(line_kwargs, "line")
|
|
1342
|
+
|
|
1343
|
+
_, df["__label"] = utils._source_and_labels_from_cats(df, cats)
|
|
1344
|
+
cols += ["__label"]
|
|
1345
|
+
|
|
1346
|
+
df = _sort_df(df, cats, order)
|
|
1347
|
+
|
|
1348
|
+
if arrangement == "stack":
|
|
1349
|
+
if cats is not None:
|
|
1350
|
+
grouped = df.groupby(cats, sort=False)
|
|
1351
|
+
return _stacked_spikes(
|
|
1352
|
+
df,
|
|
1353
|
+
grouped,
|
|
1354
|
+
q,
|
|
1355
|
+
palette,
|
|
1356
|
+
q_axis,
|
|
1357
|
+
order,
|
|
1358
|
+
p,
|
|
1359
|
+
spike_height,
|
|
1360
|
+
fraction,
|
|
1361
|
+
style,
|
|
1362
|
+
conf_int,
|
|
1363
|
+
ptiles,
|
|
1364
|
+
n_bs_reps,
|
|
1365
|
+
marker_kwargs,
|
|
1366
|
+
line_kwargs,
|
|
1367
|
+
kwargs,
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
marker_fill_color_supplied = "fill_color" in marker_kwargs
|
|
1371
|
+
marker_line_color_supplied = "line_color" in marker_kwargs
|
|
1372
|
+
line_line_color_supplied = "line_color" in line_kwargs
|
|
1373
|
+
|
|
1374
|
+
if p is None:
|
|
1375
|
+
kwargs = utils._fig_dimensions(kwargs)
|
|
1376
|
+
|
|
1377
|
+
if "x_axis_label" not in kwargs:
|
|
1378
|
+
if q_axis == "y":
|
|
1379
|
+
if fraction:
|
|
1380
|
+
kwargs["x_axis_label"] = "fraction"
|
|
1381
|
+
else:
|
|
1382
|
+
kwargs["x_axis_label"] = "count"
|
|
1383
|
+
else:
|
|
1384
|
+
kwargs["x_axis_label"] = q
|
|
1385
|
+
|
|
1386
|
+
if "y_axis_label" not in kwargs:
|
|
1387
|
+
if q_axis == "y":
|
|
1388
|
+
kwargs["y_axis_label"] = q
|
|
1389
|
+
else:
|
|
1390
|
+
if fraction:
|
|
1391
|
+
kwargs["y_axis_label"] = "fraction"
|
|
1392
|
+
else:
|
|
1393
|
+
kwargs["y_axis_label"] = "count"
|
|
1394
|
+
|
|
1395
|
+
if q_axis == "y":
|
|
1396
|
+
if "x_range" not in kwargs:
|
|
1397
|
+
kwargs["x_range"] = bokeh.models.DataRange1d(start=0)
|
|
1398
|
+
else:
|
|
1399
|
+
if "y_range" not in kwargs:
|
|
1400
|
+
kwargs["y_range"] = bokeh.models.DataRange1d(start=0)
|
|
1401
|
+
|
|
1402
|
+
p = bokeh.plotting.figure(**kwargs)
|
|
1403
|
+
|
|
1404
|
+
# Integer ticks of counts
|
|
1405
|
+
if not fraction:
|
|
1406
|
+
if q_axis == "x":
|
|
1407
|
+
p.yaxis.ticker = bokeh.models.BasicTicker(
|
|
1408
|
+
min_interval=1, num_minor_ticks=0
|
|
1409
|
+
)
|
|
1410
|
+
else:
|
|
1411
|
+
p.xaxis.ticker = bokeh.models.BasicTicker(
|
|
1412
|
+
min_interval=1, num_minor_ticks=0
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
# Explicitly loop to enable click policies on the legend (not possible with factors)
|
|
1416
|
+
lines = []
|
|
1417
|
+
markers = []
|
|
1418
|
+
labels = []
|
|
1419
|
+
|
|
1420
|
+
# Confidence intervals
|
|
1421
|
+
if conf_int:
|
|
1422
|
+
|
|
1423
|
+
@njit
|
|
1424
|
+
def _counts(ar, vals, frac):
|
|
1425
|
+
output = np.zeros(len(vals))
|
|
1426
|
+
for a in ar:
|
|
1427
|
+
output[np.searchsorted(vals, a)] += 1.0
|
|
1428
|
+
|
|
1429
|
+
if frac:
|
|
1430
|
+
return output / len(ar)
|
|
1431
|
+
else:
|
|
1432
|
+
return output
|
|
1433
|
+
|
|
1434
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
1435
|
+
x = g[q].values
|
|
1436
|
+
x_unique = np.unique(x)
|
|
1437
|
+
|
|
1438
|
+
bs_reps = [
|
|
1439
|
+
_counts(
|
|
1440
|
+
np.random.choice(x, replace=True, size=len(x)), x_unique, fraction
|
|
1441
|
+
)
|
|
1442
|
+
for _ in range(n_bs_reps)
|
|
1443
|
+
]
|
|
1444
|
+
|
|
1445
|
+
conf_ints = np.percentile(bs_reps, ptiles, axis=0)
|
|
1446
|
+
|
|
1447
|
+
df_conf_int = pd.DataFrame(
|
|
1448
|
+
{
|
|
1449
|
+
q: x_unique,
|
|
1450
|
+
"__conf_low": conf_ints[0, :],
|
|
1451
|
+
"__conf_high": conf_ints[1, :],
|
|
1452
|
+
}
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
if not line_line_color_supplied:
|
|
1456
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
1457
|
+
|
|
1458
|
+
if q_axis == "y":
|
|
1459
|
+
lines.append(
|
|
1460
|
+
p.segment(
|
|
1461
|
+
x0="__conf_low",
|
|
1462
|
+
x1="__conf_high",
|
|
1463
|
+
y0=q,
|
|
1464
|
+
y1=q,
|
|
1465
|
+
source=df_conf_int,
|
|
1466
|
+
**line_kwargs,
|
|
1467
|
+
)
|
|
1468
|
+
)
|
|
1469
|
+
else:
|
|
1470
|
+
lines.append(
|
|
1471
|
+
p.segment(
|
|
1472
|
+
x0=q,
|
|
1473
|
+
x1=q,
|
|
1474
|
+
y0="__conf_low",
|
|
1475
|
+
y1="__conf_high",
|
|
1476
|
+
source=df_conf_int,
|
|
1477
|
+
**line_kwargs,
|
|
1478
|
+
)
|
|
1479
|
+
)
|
|
1480
|
+
labels.append(g["__label"].iloc[0])
|
|
1481
|
+
|
|
1482
|
+
# Spikes
|
|
1483
|
+
if "spike" in style:
|
|
1484
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
1485
|
+
# Because of changes in how value_counts works, we have to be careful about
|
|
1486
|
+
# renaming columns and indexes.
|
|
1487
|
+
# See https://pandas.pydata.org/docs/dev/whatsnew/v2.0.0.html#value-counts-sets-the-resulting-name-to-count)
|
|
1488
|
+
df_count = g[q].value_counts().reset_index()
|
|
1489
|
+
if pd.__version__ >= "2.0.0":
|
|
1490
|
+
df_count = df_count.rename(columns={"count": "__count"})
|
|
1491
|
+
else:
|
|
1492
|
+
df_count = df_count.rename(columns={"index": q, q: "__count"})
|
|
1493
|
+
|
|
1494
|
+
if fraction:
|
|
1495
|
+
df_count["__count"] /= df_count["__count"].sum()
|
|
1496
|
+
|
|
1497
|
+
if not line_line_color_supplied:
|
|
1498
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
1499
|
+
|
|
1500
|
+
if q_axis == "y":
|
|
1501
|
+
lines.append(
|
|
1502
|
+
p.segment(
|
|
1503
|
+
x0=0, x1="__count", y0=q, y1=q, source=df_count, **line_kwargs
|
|
1504
|
+
)
|
|
1505
|
+
)
|
|
1506
|
+
else:
|
|
1507
|
+
lines.append(
|
|
1508
|
+
p.segment(
|
|
1509
|
+
x0=q, x1=q, y0=0, y1="__count", source=df_count, **line_kwargs
|
|
1510
|
+
)
|
|
1511
|
+
)
|
|
1512
|
+
labels.append(g["__label"].iloc[0])
|
|
1513
|
+
|
|
1514
|
+
# Overlay dots
|
|
1515
|
+
if "dot" in style:
|
|
1516
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
1517
|
+
df_count = g[q].value_counts().reset_index()
|
|
1518
|
+
if pd.__version__ >= "2.0.0":
|
|
1519
|
+
df_count = df_count.rename(columns={"count": "__count"})
|
|
1520
|
+
else:
|
|
1521
|
+
df_count = df_count.rename(columns={"index": q, q: "__count"})
|
|
1522
|
+
|
|
1523
|
+
if fraction:
|
|
1524
|
+
df_count["__count"] /= df_count["__count"].sum()
|
|
1525
|
+
|
|
1526
|
+
if not marker_line_color_supplied:
|
|
1527
|
+
marker_kwargs["line_color"] = palette[i % len(palette)]
|
|
1528
|
+
if not marker_fill_color_supplied:
|
|
1529
|
+
marker_kwargs["fill_color"] = palette[i % len(palette)]
|
|
1530
|
+
|
|
1531
|
+
if q_axis == "y":
|
|
1532
|
+
markers.append(
|
|
1533
|
+
p.scatter(
|
|
1534
|
+
x="__count",
|
|
1535
|
+
y=q,
|
|
1536
|
+
source=df_count,
|
|
1537
|
+
marker="circle",
|
|
1538
|
+
**marker_kwargs,
|
|
1539
|
+
)
|
|
1540
|
+
)
|
|
1541
|
+
else:
|
|
1542
|
+
markers.append(
|
|
1543
|
+
p.scatter(
|
|
1544
|
+
x=q,
|
|
1545
|
+
y="__count",
|
|
1546
|
+
source=df_count,
|
|
1547
|
+
marker="circle",
|
|
1548
|
+
**marker_kwargs,
|
|
1549
|
+
)
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
labels.append(g["__label"].iloc[0])
|
|
1553
|
+
|
|
1554
|
+
return _dist_legend(
|
|
1555
|
+
p,
|
|
1556
|
+
show_legend,
|
|
1557
|
+
legend_location,
|
|
1558
|
+
legend_orientation,
|
|
1559
|
+
legend_click_policy,
|
|
1560
|
+
labels,
|
|
1561
|
+
markers,
|
|
1562
|
+
lines,
|
|
1563
|
+
[],
|
|
1564
|
+
[],
|
|
1565
|
+
[],
|
|
1566
|
+
[],
|
|
1567
|
+
[],
|
|
1568
|
+
[],
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def _staircase_ecdf(p, data, complementary=False, q_axis="x", line_kwargs={}):
|
|
1573
|
+
"""
|
|
1574
|
+
Create a plot of an ECDF.
|
|
1575
|
+
|
|
1576
|
+
Parameters
|
|
1577
|
+
----------
|
|
1578
|
+
p : bokeh.plotting.Figure instance, or None (default)
|
|
1579
|
+
If None, create a new figure. Otherwise, populate the existing
|
|
1580
|
+
figure `p`.
|
|
1581
|
+
data : array_like
|
|
1582
|
+
One-dimensional array of data. NaNs are ignored.
|
|
1583
|
+
complementary : bool, default False
|
|
1584
|
+
If True, plot the empirical complementary cumulative
|
|
1585
|
+
distribution function.
|
|
1586
|
+
q_axis : str, default 'x'
|
|
1587
|
+
Which axis has the quantitative variable.
|
|
1588
|
+
line_kwargs : dict
|
|
1589
|
+
kwargs to be passed into p.line and p.ray.
|
|
1590
|
+
|
|
1591
|
+
Returns
|
|
1592
|
+
-------
|
|
1593
|
+
p : bokeh.plotting.Figure instance
|
|
1594
|
+
Plot populated with ECDF.
|
|
1595
|
+
line : bokeh.models.GlyphRenderer instance
|
|
1596
|
+
Line of staircase, used for constructing clickable legend.
|
|
1597
|
+
ray_high : bokeh.models.GlyphRenderer instance
|
|
1598
|
+
Ray for top of ECDF, used for constructing clickable legend.
|
|
1599
|
+
ray_low : bokeh.models.GlyphRenderer instance
|
|
1600
|
+
Ray for bottom of ECDF, used for constructing clickable legend.
|
|
1601
|
+
"""
|
|
1602
|
+
# Extract data
|
|
1603
|
+
data = utils._convert_data(data)
|
|
1604
|
+
|
|
1605
|
+
# Data points on ECDF
|
|
1606
|
+
x, y = _ecdf_vals(data, True, complementary)
|
|
1607
|
+
|
|
1608
|
+
# Line of steps
|
|
1609
|
+
if q_axis == "y":
|
|
1610
|
+
line = p.line(y, x, **line_kwargs)
|
|
1611
|
+
elif q_axis == "x":
|
|
1612
|
+
line = p.line(x, y, **line_kwargs)
|
|
1613
|
+
|
|
1614
|
+
# Rays for ends
|
|
1615
|
+
if q_axis == "y":
|
|
1616
|
+
if complementary:
|
|
1617
|
+
ray_high = p.ray(x=1, y=x[0], length=0, angle=-np.pi / 2, **line_kwargs)
|
|
1618
|
+
ray_low = p.ray(x=0, y=x[-1], length=0, angle=np.pi / 2, **line_kwargs)
|
|
1619
|
+
else:
|
|
1620
|
+
ray_low = p.ray(x=0, y=x[0], length=0, angle=-np.pi / 2, **line_kwargs)
|
|
1621
|
+
ray_high = p.ray(x=1, y=x[-1], length=0, angle=np.pi / 2, **line_kwargs)
|
|
1622
|
+
elif q_axis == "x":
|
|
1623
|
+
if complementary:
|
|
1624
|
+
ray_high = p.ray(x=x[0], y=1, length=0, angle=np.pi, **line_kwargs)
|
|
1625
|
+
ray_low = p.ray(x=x[-1], y=0, length=0, angle=0, **line_kwargs)
|
|
1626
|
+
else:
|
|
1627
|
+
ray_low = p.ray(x=x[0], y=0, length=0, angle=np.pi, **line_kwargs)
|
|
1628
|
+
ray_high = p.ray(x=x[-1], y=1, length=0, angle=0, **line_kwargs)
|
|
1629
|
+
|
|
1630
|
+
return p, line, ray_high, ray_low
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def _formal_ecdf(
|
|
1634
|
+
p, data, complementary=False, q_axis="x", marker_kwargs={}, line_kwargs={}
|
|
1635
|
+
):
|
|
1636
|
+
"""
|
|
1637
|
+
Create a plot of an ECDF.
|
|
1638
|
+
|
|
1639
|
+
Parameters
|
|
1640
|
+
----------
|
|
1641
|
+
p : bokeh.plotting.Figure instance, or None (default)
|
|
1642
|
+
If None, create a new figure. Otherwise, populate the existing
|
|
1643
|
+
figure `p`.
|
|
1644
|
+
data : array_like
|
|
1645
|
+
One-dimensional array of data. NaNs are ignored.
|
|
1646
|
+
complementary : bool, default False
|
|
1647
|
+
If True, plot the empirical complementary cumulative
|
|
1648
|
+
distribution function.
|
|
1649
|
+
q_axis : str, default 'x'
|
|
1650
|
+
Which axis has the quantitative variable.
|
|
1651
|
+
marker_kwargs : dict
|
|
1652
|
+
Any kwargs to be passed to p.scatter().
|
|
1653
|
+
line_kwargs : dict
|
|
1654
|
+
Any kwargs to be passed to p.segment() and p.ray().
|
|
1655
|
+
|
|
1656
|
+
Returns
|
|
1657
|
+
-------
|
|
1658
|
+
p : bokeh.plotting.Figure instance
|
|
1659
|
+
Plot populated with ECDF.
|
|
1660
|
+
segment : bokeh.models.GlyphRenderer instance
|
|
1661
|
+
Line of staircase, used for constructing clickable legend.
|
|
1662
|
+
ray_high : bokeh.models.GlyphRenderer instance
|
|
1663
|
+
Ray for top of ECDF, used for constructing clickable legend.
|
|
1664
|
+
ray_low : bokeh.models.GlyphRenderer instance
|
|
1665
|
+
Ray for bottom of ECDF, used for constructing clickable legend.
|
|
1666
|
+
circle_high : bokeh.models.GlyphRenderer instance
|
|
1667
|
+
Open circle for top of ECDF, used for constructing clickable
|
|
1668
|
+
legend.
|
|
1669
|
+
circle_low : bokeh.models.GlyphRenderer instance
|
|
1670
|
+
Open circle for bottom of ECDF, used for constructing clickable
|
|
1671
|
+
legend.
|
|
1672
|
+
"""
|
|
1673
|
+
# Extract data
|
|
1674
|
+
data = utils._convert_data(data)
|
|
1675
|
+
|
|
1676
|
+
# Data points on ECDF
|
|
1677
|
+
x, y = _ecdf_vals(data, complementary)
|
|
1678
|
+
|
|
1679
|
+
# Copy of marker kwargs for unfilled points
|
|
1680
|
+
unfilled_kwargs = marker_kwargs.copy()
|
|
1681
|
+
unfilled_kwargs["fill_color"] = "white"
|
|
1682
|
+
|
|
1683
|
+
if q_axis == "y":
|
|
1684
|
+
segment = p.segment(y[:-1], x[:-1], y[1:], x[:-1], **line_kwargs)
|
|
1685
|
+
ray_low = p.ray(x=0, y=x[0], angle=-np.pi / 2, length=0, **line_kwargs)
|
|
1686
|
+
ray_high = p.ray(x=1, y=x[-1], angle=np.pi / 2, length=0, **line_kwargs)
|
|
1687
|
+
circle = p.scatter(y, x, marker="circle", **marker_kwargs)
|
|
1688
|
+
circle_low = p.scatter([0], [0], marker="circle", **unfilled_kwargs)
|
|
1689
|
+
circle_high = p.scatter(y[:-1], x[1:], marker="circle", **unfilled_kwargs)
|
|
1690
|
+
elif q_axis == "x":
|
|
1691
|
+
segment = p.segment(x[:-1], y[:-1], x[1:], y[:-1], **line_kwargs)
|
|
1692
|
+
ray_low = p.ray(x=x[0], y=0, angle=np.pi, length=0, **line_kwargs)
|
|
1693
|
+
ray_high = p.ray(x=x[-1], y=1, angle=0, length=0, **line_kwargs)
|
|
1694
|
+
circle = p.scatter(x, y, marker="circle", **marker_kwargs)
|
|
1695
|
+
circle_low = p.scatter([0], [0], marker="circle", **unfilled_kwargs)
|
|
1696
|
+
circle_high = p.scatter(x[1:], y[:-1], marker="circle", **unfilled_kwargs)
|
|
1697
|
+
|
|
1698
|
+
return p, circle, segment, ray_high, ray_low, circle_high, circle_low
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def _ecdf_vals(data, staircase=False, complementary=False):
|
|
1702
|
+
"""Get x, y values of an ECDF for plotting.
|
|
1703
|
+
|
|
1704
|
+
Parameters
|
|
1705
|
+
----------
|
|
1706
|
+
data : ndarray
|
|
1707
|
+
One dimensional Numpy array with data.
|
|
1708
|
+
staircase : bool, default False
|
|
1709
|
+
If True, generate x and y values for ECDF (staircase). If
|
|
1710
|
+
False, generate x and y values for ECDF as dots.
|
|
1711
|
+
complementary : bool
|
|
1712
|
+
If True, return values for ECCDF.
|
|
1713
|
+
|
|
1714
|
+
Returns
|
|
1715
|
+
-------
|
|
1716
|
+
x : ndarray
|
|
1717
|
+
x-values for plot
|
|
1718
|
+
y : ndarray
|
|
1719
|
+
y-values for plot
|
|
1720
|
+
"""
|
|
1721
|
+
x = np.sort(data)
|
|
1722
|
+
y = np.arange(1, len(data) + 1) / len(data)
|
|
1723
|
+
|
|
1724
|
+
if staircase:
|
|
1725
|
+
x, y = _to_staircase(x, y)
|
|
1726
|
+
if complementary:
|
|
1727
|
+
y = 1 - y
|
|
1728
|
+
elif complementary:
|
|
1729
|
+
y = 1 - y + 1 / len(y)
|
|
1730
|
+
|
|
1731
|
+
return x, y
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def _to_staircase(x, y):
|
|
1735
|
+
"""Convert to formal ECDF."""
|
|
1736
|
+
# Set up output arrays
|
|
1737
|
+
x_staircase = np.empty(2 * len(x))
|
|
1738
|
+
y_staircase = np.empty(2 * len(x))
|
|
1739
|
+
|
|
1740
|
+
# y-values for steps
|
|
1741
|
+
y_staircase[0] = 0
|
|
1742
|
+
y_staircase[1::2] = y
|
|
1743
|
+
y_staircase[2::2] = y[:-1]
|
|
1744
|
+
|
|
1745
|
+
# x-values for steps
|
|
1746
|
+
x_staircase[::2] = x
|
|
1747
|
+
x_staircase[1::2] = x
|
|
1748
|
+
|
|
1749
|
+
return x_staircase, y_staircase
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def _stacked_ecdfs(
|
|
1753
|
+
data,
|
|
1754
|
+
q=None,
|
|
1755
|
+
cats=None,
|
|
1756
|
+
q_axis="x",
|
|
1757
|
+
palette=None,
|
|
1758
|
+
order=None,
|
|
1759
|
+
tooltips=None,
|
|
1760
|
+
complementary=False,
|
|
1761
|
+
kind="collection",
|
|
1762
|
+
style="dots",
|
|
1763
|
+
conf_int=False,
|
|
1764
|
+
ptiles=(2.5, 97.5),
|
|
1765
|
+
n_bs_reps=10000,
|
|
1766
|
+
marker="circle",
|
|
1767
|
+
marker_kwargs=None,
|
|
1768
|
+
line_kwargs=None,
|
|
1769
|
+
fill_kwargs=None,
|
|
1770
|
+
**kwargs,
|
|
1771
|
+
):
|
|
1772
|
+
ps = []
|
|
1773
|
+
|
|
1774
|
+
if isinstance(cats, (list, tuple)) and len(cats) == 1:
|
|
1775
|
+
cats = cats[0]
|
|
1776
|
+
|
|
1777
|
+
# Protect against mutability and get copies
|
|
1778
|
+
df = data.copy()
|
|
1779
|
+
kwargs = copy.copy(kwargs)
|
|
1780
|
+
marker_kwargs = copy.copy(marker_kwargs)
|
|
1781
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
1782
|
+
fill_kwargs = copy.copy(fill_kwargs)
|
|
1783
|
+
|
|
1784
|
+
df = _sort_df(df, cats, order)
|
|
1785
|
+
|
|
1786
|
+
if (
|
|
1787
|
+
"frame_width" not in kwargs
|
|
1788
|
+
and "width" not in kwargs
|
|
1789
|
+
and "plot_width" not in kwargs
|
|
1790
|
+
):
|
|
1791
|
+
if q_axis == "y":
|
|
1792
|
+
kwargs["frame_width"] = 100
|
|
1793
|
+
if (
|
|
1794
|
+
"frame_height" not in kwargs
|
|
1795
|
+
and "height" not in kwargs
|
|
1796
|
+
and "plot_height" not in kwargs
|
|
1797
|
+
):
|
|
1798
|
+
if q_axis == "x":
|
|
1799
|
+
kwargs["frame_height"] = 100
|
|
1800
|
+
if "min_border" not in kwargs:
|
|
1801
|
+
kwargs["min_border"] = kwargs.pop("min_border", 5)
|
|
1802
|
+
|
|
1803
|
+
marker_kwargs = utils._check_marker_kwargs(marker_kwargs)
|
|
1804
|
+
if line_kwargs is None:
|
|
1805
|
+
line_kwargs = {}
|
|
1806
|
+
if fill_kwargs is None:
|
|
1807
|
+
fill_kwargs = {}
|
|
1808
|
+
|
|
1809
|
+
marker_fill_color_supplied = "fill_color" in marker_kwargs
|
|
1810
|
+
marker_line_color_supplied = "line_color" in marker_kwargs
|
|
1811
|
+
line_line_color_supplied = "line_color" in line_kwargs
|
|
1812
|
+
fill_fill_color_supplied = "fill_color" in fill_kwargs
|
|
1813
|
+
|
|
1814
|
+
title = kwargs.pop("title", None)
|
|
1815
|
+
if title is not None and q_axis == "y":
|
|
1816
|
+
raise RuntimeError(
|
|
1817
|
+
"`title` is not an allowed kwarg when q_axis is 'y' and `arrangment` is 'stack'."
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
for i, (name, g) in enumerate(df.groupby(cats, sort=False)):
|
|
1821
|
+
color = palette[i % len(palette)]
|
|
1822
|
+
if not marker_fill_color_supplied:
|
|
1823
|
+
marker_kwargs["fill_color"] = color
|
|
1824
|
+
if not marker_line_color_supplied:
|
|
1825
|
+
marker_kwargs["line_color"] = color
|
|
1826
|
+
if not line_line_color_supplied:
|
|
1827
|
+
line_kwargs["line_color"] = color
|
|
1828
|
+
if not fill_fill_color_supplied:
|
|
1829
|
+
fill_kwargs["fill_color"] = color
|
|
1830
|
+
|
|
1831
|
+
if q_axis == "x":
|
|
1832
|
+
kwargs["y_axis_label"] = str(name)
|
|
1833
|
+
if i == 0:
|
|
1834
|
+
kwargs["title"] = title
|
|
1835
|
+
else:
|
|
1836
|
+
kwargs["title"] = None
|
|
1837
|
+
else:
|
|
1838
|
+
kwargs["x_axis_label"] = q
|
|
1839
|
+
kwargs["title"] = str(name)
|
|
1840
|
+
|
|
1841
|
+
ps.append(
|
|
1842
|
+
ecdf(
|
|
1843
|
+
data=g,
|
|
1844
|
+
q=q,
|
|
1845
|
+
q_axis=q_axis,
|
|
1846
|
+
tooltips=tooltips,
|
|
1847
|
+
complementary=complementary,
|
|
1848
|
+
kind=kind,
|
|
1849
|
+
style=style,
|
|
1850
|
+
conf_int=conf_int,
|
|
1851
|
+
ptiles=ptiles,
|
|
1852
|
+
n_bs_reps=n_bs_reps,
|
|
1853
|
+
marker=marker,
|
|
1854
|
+
marker_kwargs=marker_kwargs,
|
|
1855
|
+
line_kwargs=line_kwargs,
|
|
1856
|
+
fill_kwargs=fill_kwargs,
|
|
1857
|
+
**kwargs,
|
|
1858
|
+
)
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
if q_axis == "x":
|
|
1862
|
+
for i, _ in enumerate(ps[:-1]):
|
|
1863
|
+
ps[i].xaxis.visible = False
|
|
1864
|
+
ps[i].xaxis.axis_label = None
|
|
1865
|
+
ps[i].x_range = ps[-1].x_range
|
|
1866
|
+
ps[i].y_range = ps[-1].y_range
|
|
1867
|
+
|
|
1868
|
+
for i, _ in enumerate(ps):
|
|
1869
|
+
ps[i].yaxis.minor_tick_out = 0
|
|
1870
|
+
ps[i].yaxis.axis_label_text_font_style = "bold"
|
|
1871
|
+
ps[i].yaxis.axis_label_text_color = "#696969"
|
|
1872
|
+
|
|
1873
|
+
return bokeh.layouts.gridplot(ps, ncols=1)
|
|
1874
|
+
else:
|
|
1875
|
+
for i, _ in enumerate(ps[1:]):
|
|
1876
|
+
ps[i + 1].yaxis.visible = False
|
|
1877
|
+
ps[i + 1].yaxis.axis_label = None
|
|
1878
|
+
ps[i + 1].x_range = ps[0].x_range
|
|
1879
|
+
ps[i + 1].y_range = ps[0].y_range
|
|
1880
|
+
|
|
1881
|
+
for i, _ in enumerate(ps):
|
|
1882
|
+
ps[i].xaxis.minor_tick_out = 0
|
|
1883
|
+
ps[i].xaxis.major_label_orientation = np.pi / 3
|
|
1884
|
+
ps[i].title.align = "center"
|
|
1885
|
+
ps[i].title.text_font_style = "bold"
|
|
1886
|
+
ps[i].title.text_color = "#696969"
|
|
1887
|
+
|
|
1888
|
+
return bokeh.layouts.gridplot(ps, ncols=len(ps))
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _stacked_histograms(
|
|
1892
|
+
df,
|
|
1893
|
+
grouped,
|
|
1894
|
+
q,
|
|
1895
|
+
bins,
|
|
1896
|
+
density,
|
|
1897
|
+
palette,
|
|
1898
|
+
q_axis,
|
|
1899
|
+
order,
|
|
1900
|
+
p,
|
|
1901
|
+
mirror,
|
|
1902
|
+
hist_height,
|
|
1903
|
+
style,
|
|
1904
|
+
conf_int,
|
|
1905
|
+
ptiles,
|
|
1906
|
+
n_bs_reps,
|
|
1907
|
+
rug,
|
|
1908
|
+
rug_height,
|
|
1909
|
+
tooltips,
|
|
1910
|
+
line_kwargs,
|
|
1911
|
+
fill_kwargs,
|
|
1912
|
+
binsep_kwargs,
|
|
1913
|
+
rug_kwargs,
|
|
1914
|
+
kwargs,
|
|
1915
|
+
):
|
|
1916
|
+
# Protect against mutability and get copies
|
|
1917
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
1918
|
+
fill_kwargs = copy.copy(fill_kwargs)
|
|
1919
|
+
binsep_kwargs = copy.copy(binsep_kwargs)
|
|
1920
|
+
|
|
1921
|
+
line_line_color_supplied = "line_color" in line_kwargs
|
|
1922
|
+
fill_fill_color_supplied = "fill_color" in fill_kwargs
|
|
1923
|
+
binsep_color_supplied = "line_color" in binsep_kwargs or "color" in binsep_kwargs
|
|
1924
|
+
|
|
1925
|
+
if p is None:
|
|
1926
|
+
p, _, _ = cat._cat_figure(df, grouped, q, order, None, q_axis, kwargs)
|
|
1927
|
+
|
|
1928
|
+
f0_max = 0.0
|
|
1929
|
+
plot_data = {}
|
|
1930
|
+
for i, (name, g) in enumerate(grouped):
|
|
1931
|
+
numerical_bins, e, f = _compute_histogram(g[q].values, bins, density)
|
|
1932
|
+
e0, f0 = _hist_for_plotting(e, f)
|
|
1933
|
+
|
|
1934
|
+
plot_data[name] = dict(e0=e0, f0=f0)
|
|
1935
|
+
|
|
1936
|
+
# Record f0_max
|
|
1937
|
+
f0_max = max(f0_max, f0.max())
|
|
1938
|
+
|
|
1939
|
+
if conf_int:
|
|
1940
|
+
q_vals = g[q].values
|
|
1941
|
+
f_reps = np.empty((n_bs_reps, len(f)))
|
|
1942
|
+
for j in range(n_bs_reps):
|
|
1943
|
+
_, _, f_reps[j, :] = _compute_histogram(
|
|
1944
|
+
np.random.choice(q_vals, len(q_vals)), numerical_bins, density
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1947
|
+
f_ptiles = np.percentile(f_reps, ptiles, axis=0)
|
|
1948
|
+
|
|
1949
|
+
_, f0_low = _hist_for_plotting(e, f_ptiles[0, :])
|
|
1950
|
+
_, f0_high = _hist_for_plotting(e, f_ptiles[1, :])
|
|
1951
|
+
|
|
1952
|
+
# Store the plot data
|
|
1953
|
+
plot_data[name]["f0_low"] = f0_low
|
|
1954
|
+
plot_data[name]["f0_high"] = f0_high
|
|
1955
|
+
|
|
1956
|
+
# Record max f0
|
|
1957
|
+
f0_max = max(f0_max, f0_high.max())
|
|
1958
|
+
|
|
1959
|
+
if not density:
|
|
1960
|
+
scale = 1.0 / f0_max * hist_height / 2
|
|
1961
|
+
|
|
1962
|
+
for i, (name, plot_data_dict) in enumerate(plot_data.items()):
|
|
1963
|
+
if not fill_fill_color_supplied:
|
|
1964
|
+
fill_kwargs["fill_color"] = palette[i % len(palette)]
|
|
1965
|
+
if not line_line_color_supplied:
|
|
1966
|
+
line_kwargs["line_color"] = palette[i % len(palette)]
|
|
1967
|
+
|
|
1968
|
+
if density:
|
|
1969
|
+
if conf_int:
|
|
1970
|
+
scale = 1.0 / plot_data_dict["f0_high"].max() * hist_height / 2
|
|
1971
|
+
else:
|
|
1972
|
+
scale = 1.0 / plot_data_dict["f0"].max() * hist_height / 2
|
|
1973
|
+
|
|
1974
|
+
if conf_int:
|
|
1975
|
+
f0_low_cat = [
|
|
1976
|
+
(*name, f0_val) if isinstance(name, tuple) else (name, f0_val)
|
|
1977
|
+
for f0_val in scale * plot_data_dict["f0_low"]
|
|
1978
|
+
]
|
|
1979
|
+
f0_high_cat = [
|
|
1980
|
+
(*name, f0_val) if isinstance(name, tuple) else (name, f0_val)
|
|
1981
|
+
for f0_val in scale * plot_data_dict["f0_high"]
|
|
1982
|
+
]
|
|
1983
|
+
|
|
1984
|
+
if q_axis == "y":
|
|
1985
|
+
p, patch = utils._fill_between(
|
|
1986
|
+
p,
|
|
1987
|
+
f0_low_cat,
|
|
1988
|
+
plot_data_dict["e0"],
|
|
1989
|
+
f0_high_cat,
|
|
1990
|
+
plot_data_dict["e0"],
|
|
1991
|
+
**fill_kwargs,
|
|
1992
|
+
)
|
|
1993
|
+
else:
|
|
1994
|
+
p, patch = utils._fill_between(
|
|
1995
|
+
p,
|
|
1996
|
+
plot_data_dict["e0"],
|
|
1997
|
+
f0_low_cat,
|
|
1998
|
+
plot_data_dict["e0"],
|
|
1999
|
+
f0_high_cat,
|
|
2000
|
+
**fill_kwargs,
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
if mirror:
|
|
2004
|
+
f0_low_cat = [
|
|
2005
|
+
(*name, f0_val) if isinstance(name, tuple) else (name, f0_val)
|
|
2006
|
+
for f0_val in -scale * plot_data_dict["f0_low"]
|
|
2007
|
+
]
|
|
2008
|
+
f0_high_cat = [
|
|
2009
|
+
(*name, f0_val) if isinstance(name, tuple) else (name, f0_val)
|
|
2010
|
+
for f0_val in -scale * plot_data_dict["f0_high"]
|
|
2011
|
+
]
|
|
2012
|
+
if q_axis == "y":
|
|
2013
|
+
p, patch = utils._fill_between(
|
|
2014
|
+
p,
|
|
2015
|
+
f0_low_cat,
|
|
2016
|
+
plot_data_dict["e0"],
|
|
2017
|
+
f0_high_cat,
|
|
2018
|
+
plot_data_dict["e0"],
|
|
2019
|
+
**fill_kwargs,
|
|
2020
|
+
)
|
|
2021
|
+
else:
|
|
2022
|
+
p, patch = utils._fill_between(
|
|
2023
|
+
p,
|
|
2024
|
+
plot_data_dict["e0"],
|
|
2025
|
+
f0_low_cat,
|
|
2026
|
+
plot_data_dict["e0"],
|
|
2027
|
+
f0_high_cat,
|
|
2028
|
+
**fill_kwargs,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
# y-values for histogram, appropriately scaled
|
|
2032
|
+
f0 = plot_data_dict["f0"] * scale
|
|
2033
|
+
f0_cat = [
|
|
2034
|
+
(*name, f0_val) if isinstance(name, tuple) else (name, f0_val)
|
|
2035
|
+
for f0_val in f0
|
|
2036
|
+
]
|
|
2037
|
+
|
|
2038
|
+
if mirror:
|
|
2039
|
+
f0_cat += list(
|
|
2040
|
+
reversed(
|
|
2041
|
+
[
|
|
2042
|
+
(*name, -f0_val) if isinstance(name, tuple) else (name, -f0_val)
|
|
2043
|
+
for f0_val in f0
|
|
2044
|
+
]
|
|
2045
|
+
)
|
|
2046
|
+
)
|
|
2047
|
+
e0 = np.concatenate((plot_data_dict["e0"], plot_data_dict["e0"][::-1]))
|
|
2048
|
+
else:
|
|
2049
|
+
e0 = plot_data_dict["e0"]
|
|
2050
|
+
|
|
2051
|
+
# Line of histogram
|
|
2052
|
+
if q_axis == "y":
|
|
2053
|
+
p.line(f0_cat, e0, **line_kwargs)
|
|
2054
|
+
|
|
2055
|
+
if style == "step_filled":
|
|
2056
|
+
p.patch(f0_cat, e0, **fill_kwargs)
|
|
2057
|
+
else:
|
|
2058
|
+
p.line(e0, f0_cat, **line_kwargs)
|
|
2059
|
+
if style == "step_filled":
|
|
2060
|
+
p.patch(e0, f0_cat, **fill_kwargs)
|
|
2061
|
+
|
|
2062
|
+
# Bin separators
|
|
2063
|
+
if not binsep_color_supplied:
|
|
2064
|
+
binsep_kwargs["color"] = palette[i % len(palette)]
|
|
2065
|
+
if binsep_kwargs["line_alpha"] > 0:
|
|
2066
|
+
# Heights are computed from the unmirrored histogram, since the
|
|
2067
|
+
# mirrored one traverses each bin edge twice.
|
|
2068
|
+
heights = [min(f0[j], f0[j + 1]) for j in range(2, len(f0) - 2, 2)]
|
|
2069
|
+
|
|
2070
|
+
binsep_cat = [
|
|
2071
|
+
(
|
|
2072
|
+
[
|
|
2073
|
+
(
|
|
2074
|
+
(*name, -height)
|
|
2075
|
+
if isinstance(name, tuple)
|
|
2076
|
+
else (name, -height)
|
|
2077
|
+
),
|
|
2078
|
+
(*name, height) if isinstance(name, tuple) else (name, height),
|
|
2079
|
+
]
|
|
2080
|
+
if mirror
|
|
2081
|
+
else [
|
|
2082
|
+
(*name, 0.0) if isinstance(name, tuple) else (name, 0.0),
|
|
2083
|
+
(*name, height) if isinstance(name, tuple) else (name, height),
|
|
2084
|
+
]
|
|
2085
|
+
)
|
|
2086
|
+
for height in heights
|
|
2087
|
+
]
|
|
2088
|
+
binsep_q = [[edge, edge] for edge in plot_data_dict["e0"][2:-2:2]]
|
|
2089
|
+
|
|
2090
|
+
if q_axis == "y":
|
|
2091
|
+
p.multi_line(binsep_cat, binsep_q, **binsep_kwargs)
|
|
2092
|
+
else:
|
|
2093
|
+
p.multi_line(binsep_q, binsep_cat, **binsep_kwargs)
|
|
2094
|
+
|
|
2095
|
+
# Add rug
|
|
2096
|
+
if rug:
|
|
2097
|
+
for i, (name, g) in enumerate(grouped):
|
|
2098
|
+
xs = [[x, x] for x in g[q]]
|
|
2099
|
+
y0_cat = [
|
|
2100
|
+
(
|
|
2101
|
+
(*name, rug_height * hist_height / 2)
|
|
2102
|
+
if isinstance(name, tuple)
|
|
2103
|
+
else (name, rug_height * hist_height / 2)
|
|
2104
|
+
)
|
|
2105
|
+
for _ in range(len(g))
|
|
2106
|
+
]
|
|
2107
|
+
|
|
2108
|
+
if mirror:
|
|
2109
|
+
ys = [(y0, y0[:-1] + (-y0[-1],)) for y0 in y0_cat]
|
|
2110
|
+
else:
|
|
2111
|
+
ys = [(y0, y0[:-1] + (0,)) for y0 in y0_cat]
|
|
2112
|
+
|
|
2113
|
+
if q_axis == "y":
|
|
2114
|
+
xs, ys = ys, xs
|
|
2115
|
+
|
|
2116
|
+
cds = bokeh.models.ColumnDataSource(g)
|
|
2117
|
+
cds.data["__xs"] = xs
|
|
2118
|
+
cds.data["__ys"] = ys
|
|
2119
|
+
|
|
2120
|
+
if "color" not in rug_kwargs and "line_color" not in rug_kwargs:
|
|
2121
|
+
p.multi_line(
|
|
2122
|
+
source=cds,
|
|
2123
|
+
xs="__xs",
|
|
2124
|
+
ys="__ys",
|
|
2125
|
+
line_color=palette[i % len(palette)],
|
|
2126
|
+
**rug_kwargs,
|
|
2127
|
+
)
|
|
2128
|
+
else:
|
|
2129
|
+
p.multi_line(source=cds, xs="__xs", ys="__ys", **rug_kwargs)
|
|
2130
|
+
|
|
2131
|
+
if rug and tooltips is not None:
|
|
2132
|
+
p.add_tools(bokeh.models.HoverTool(tooltips=tooltips, name="hover_glyphs"))
|
|
2133
|
+
|
|
2134
|
+
return p
|
|
2135
|
+
|
|
2136
|
+
|
|
2137
|
+
def _stacked_spikes(
|
|
2138
|
+
df,
|
|
2139
|
+
grouped,
|
|
2140
|
+
q,
|
|
2141
|
+
palette,
|
|
2142
|
+
q_axis,
|
|
2143
|
+
order,
|
|
2144
|
+
p,
|
|
2145
|
+
spike_height,
|
|
2146
|
+
fraction,
|
|
2147
|
+
style,
|
|
2148
|
+
conf_int,
|
|
2149
|
+
ptiles,
|
|
2150
|
+
n_bs_reps,
|
|
2151
|
+
marker_kwargs,
|
|
2152
|
+
line_kwargs,
|
|
2153
|
+
kwargs,
|
|
2154
|
+
):
|
|
2155
|
+
# Protect against mutability and get copies
|
|
2156
|
+
line_kwargs = copy.copy(line_kwargs)
|
|
2157
|
+
marker_kwargs = copy.copy(marker_kwargs)
|
|
2158
|
+
|
|
2159
|
+
line_line_color_supplied = "line_color" in line_kwargs
|
|
2160
|
+
marker_fill_color_supplied = "fill_color" in marker_kwargs
|
|
2161
|
+
marker_line_color_supplied = "line_color" in marker_kwargs
|
|
2162
|
+
|
|
2163
|
+
if p is None:
|
|
2164
|
+
p, _, _ = cat._cat_figure(df, grouped, q, order, None, q_axis, kwargs)
|
|
2165
|
+
|
|
2166
|
+
# Compute confidence intervals, keeping track of maximum possible spike height
|
|
2167
|
+
if conf_int:
|
|
2168
|
+
|
|
2169
|
+
@njit
|
|
2170
|
+
def _counts(ar, vals, frac):
|
|
2171
|
+
output = np.zeros(len(vals))
|
|
2172
|
+
for a in ar:
|
|
2173
|
+
output[np.searchsorted(vals, a)] += 1.0
|
|
2174
|
+
|
|
2175
|
+
if frac:
|
|
2176
|
+
return output / len(ar)
|
|
2177
|
+
else:
|
|
2178
|
+
return output
|
|
2179
|
+
|
|
2180
|
+
conf_ints_dict = dict()
|
|
2181
|
+
max_spike = 0
|
|
2182
|
+
for i, (name, g) in enumerate(grouped):
|
|
2183
|
+
x = g[q].values
|
|
2184
|
+
x_unique = np.unique(x)
|
|
2185
|
+
|
|
2186
|
+
bs_reps = [
|
|
2187
|
+
_counts(
|
|
2188
|
+
np.random.choice(x, replace=True, size=len(x)), x_unique, fraction
|
|
2189
|
+
)
|
|
2190
|
+
for _ in range(n_bs_reps)
|
|
2191
|
+
]
|
|
2192
|
+
|
|
2193
|
+
conf_ints = np.percentile(bs_reps, ptiles, axis=0)
|
|
2194
|
+
|
|
2195
|
+
if not fraction:
|
|
2196
|
+
max_spike = max(max_spike, conf_ints.max())
|
|
2197
|
+
|
|
2198
|
+
conf_ints_dict[name] = pd.DataFrame(
|
|
2199
|
+
{
|
|
2200
|
+
q: x_unique,
|
|
2201
|
+
"__conf_low": conf_ints[0, :],
|
|
2202
|
+
"__conf_high": conf_ints[1, :],
|
|
2203
|
+
}
|
|
2204
|
+
)
|
|
2205
|
+
elif not fraction:
|
|
2206
|
+
counts = grouped[q].value_counts().rename("__count").reset_index()
|
|
2207
|
+
max_spike = counts["__count"].max()
|
|
2208
|
+
|
|
2209
|
+
if not fraction:
|
|
2210
|
+
scale = 1.0 / max_spike * spike_height / 2
|
|
2211
|
+
|
|
2212
|
+
for i, (name, g) in enumerate(grouped):
|
|
2213
|
+
# Confidence intervals
|
|
2214
|
+
if conf_int:
|
|
2215
|
+
if fraction:
|
|
2216
|
+
scale = (
|
|
2217
|
+
1.0 / conf_ints_dict[name]["__conf_high"].max() * spike_height / 2
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
conf_ints_dict[name]["__conf_cat_low"] = [
|
|
2221
|
+
(*name, val) if isinstance(name, tuple) else (name, val)
|
|
2222
|
+
for val in scale * conf_ints_dict[name]["__conf_low"]
|
|
2223
|
+
]
|
|
2224
|
+
|
|
2225
|
+
conf_ints_dict[name]["__conf_cat_high"] = [
|
|
2226
|
+
(*name, val) if isinstance(name, tuple) else (name, val)
|
|
2227
|
+
for val in scale * conf_ints_dict[name]["__conf_high"]
|
|
2228
|
+
]
|
|
2229
|
+
|
|
2230
|
+
if not line_line_color_supplied:
|
|
2231
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
2232
|
+
|
|
2233
|
+
if q_axis == "y":
|
|
2234
|
+
p.segment(
|
|
2235
|
+
x0="__conf_cat_low",
|
|
2236
|
+
x1="__conf_cat_high",
|
|
2237
|
+
y0=q,
|
|
2238
|
+
y1=q,
|
|
2239
|
+
source=conf_ints_dict[name],
|
|
2240
|
+
**line_kwargs,
|
|
2241
|
+
)
|
|
2242
|
+
else:
|
|
2243
|
+
p.segment(
|
|
2244
|
+
x0=q,
|
|
2245
|
+
x1=q,
|
|
2246
|
+
y0="__conf_cat_low",
|
|
2247
|
+
y1="__conf_cat_high",
|
|
2248
|
+
source=conf_ints_dict[name],
|
|
2249
|
+
**line_kwargs,
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
# Make a count data frame for spikes and dots
|
|
2253
|
+
df_count = g[q].value_counts().reset_index()
|
|
2254
|
+
if pd.__version__ >= "2.0.0":
|
|
2255
|
+
df_count = df_count.rename(columns={"count": "__count"})
|
|
2256
|
+
else:
|
|
2257
|
+
df_count = df_count.rename(columns={"index": q, q: "__count"})
|
|
2258
|
+
|
|
2259
|
+
# For now, enforce fraction
|
|
2260
|
+
if fraction:
|
|
2261
|
+
df_count["__count"] /= df_count["__count"].sum()
|
|
2262
|
+
|
|
2263
|
+
# Scaling to fit properly with counting
|
|
2264
|
+
if not conf_int and fraction:
|
|
2265
|
+
scale = 1.0 / np.max(df_count["__count"]) * spike_height / 2
|
|
2266
|
+
|
|
2267
|
+
# Compute counts with the categorical value included
|
|
2268
|
+
df_count["__count_cat"] = [
|
|
2269
|
+
(*name, val) if isinstance(name, tuple) else (name, val)
|
|
2270
|
+
for val in scale * df_count["__count"]
|
|
2271
|
+
]
|
|
2272
|
+
|
|
2273
|
+
# Spikes
|
|
2274
|
+
if "spike" in style:
|
|
2275
|
+
df_count["__count_cat_base"] = [
|
|
2276
|
+
(*name, 0) if isinstance(name, tuple) else (name, 0)
|
|
2277
|
+
for _ in df_count["__count"]
|
|
2278
|
+
]
|
|
2279
|
+
|
|
2280
|
+
if not line_line_color_supplied:
|
|
2281
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
2282
|
+
|
|
2283
|
+
if q_axis == "y":
|
|
2284
|
+
p.segment(
|
|
2285
|
+
x0="__count_cat_base",
|
|
2286
|
+
x1="__count_cat",
|
|
2287
|
+
y0=q,
|
|
2288
|
+
y1=q,
|
|
2289
|
+
source=df_count,
|
|
2290
|
+
**line_kwargs,
|
|
2291
|
+
)
|
|
2292
|
+
else:
|
|
2293
|
+
p.segment(
|
|
2294
|
+
x0=q,
|
|
2295
|
+
x1=q,
|
|
2296
|
+
y0="__count_cat_base",
|
|
2297
|
+
y1="__count_cat",
|
|
2298
|
+
source=df_count,
|
|
2299
|
+
**line_kwargs,
|
|
2300
|
+
)
|
|
2301
|
+
|
|
2302
|
+
# Overlay dots
|
|
2303
|
+
if "dot" in style:
|
|
2304
|
+
if not line_line_color_supplied:
|
|
2305
|
+
line_kwargs["color"] = palette[i % len(palette)]
|
|
2306
|
+
|
|
2307
|
+
if not marker_line_color_supplied:
|
|
2308
|
+
marker_kwargs["line_color"] = palette[i % len(palette)]
|
|
2309
|
+
if not marker_fill_color_supplied:
|
|
2310
|
+
marker_kwargs["fill_color"] = palette[i % len(palette)]
|
|
2311
|
+
|
|
2312
|
+
if q_axis == "y":
|
|
2313
|
+
p.scatter(
|
|
2314
|
+
x="__count_cat",
|
|
2315
|
+
y=q,
|
|
2316
|
+
source=df_count,
|
|
2317
|
+
marker="circle",
|
|
2318
|
+
**marker_kwargs,
|
|
2319
|
+
)
|
|
2320
|
+
else:
|
|
2321
|
+
p.scatter(
|
|
2322
|
+
x=q,
|
|
2323
|
+
y="__count_cat",
|
|
2324
|
+
source=df_count,
|
|
2325
|
+
marker="circle",
|
|
2326
|
+
**marker_kwargs,
|
|
2327
|
+
)
|
|
2328
|
+
|
|
2329
|
+
return p
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
def _ecdf_conf_int(
|
|
2333
|
+
p,
|
|
2334
|
+
data,
|
|
2335
|
+
complementary=False,
|
|
2336
|
+
q_axis="x",
|
|
2337
|
+
n_bs_reps=10000,
|
|
2338
|
+
ptiles=(2.5, 97.5),
|
|
2339
|
+
**kwargs,
|
|
2340
|
+
):
|
|
2341
|
+
"""Add an ECDF confidence interval to a plot.
|
|
2342
|
+
|
|
2343
|
+
This method of computing a confidence interval can be thought of as
|
|
2344
|
+
computing confidence intervals of the *inverse* ECDF in the sense
|
|
2345
|
+
that we compute a confidence interval for the x-values for each of
|
|
2346
|
+
the discrete values of the ECDF. This is equivalent to computing
|
|
2347
|
+
bootstrap confidence intervals for the ECDF. Here is why.
|
|
2348
|
+
|
|
2349
|
+
Imagine we draw bootstrap samples and for each we make an ECDF.
|
|
2350
|
+
Let's say we make 5 such ECDFs and we wish to compute a 60%
|
|
2351
|
+
confidence interval. (You can generalize to arbitrary number of
|
|
2352
|
+
ECDFs and confidence interval.)
|
|
2353
|
+
|
|
2354
|
+
Each of these 5 ECDFs can be defined as starting at the same point
|
|
2355
|
+
and ending at the same point. Specifically, they start at
|
|
2356
|
+
x = min(data), y = 0 and end at x = max(data), y = 1. Furthermore,
|
|
2357
|
+
they are all monotonically increasing functions.
|
|
2358
|
+
|
|
2359
|
+
Now, let's say we are constructing a confidence interval for the
|
|
2360
|
+
ECDF at position x. To do so, we put a dot on the second ECDF from
|
|
2361
|
+
the top at x and a dot on the second ECDF from the bottom. This
|
|
2362
|
+
gives us the middle 60% of ECDF values.
|
|
2363
|
+
|
|
2364
|
+
Now, say we are constructing a confidence interval for the IECDF. We
|
|
2365
|
+
go to ECDF value y and we find the second ECDF from the left and
|
|
2366
|
+
place a dot on it. We also put a dot on the second ECDF from the
|
|
2367
|
+
right.
|
|
2368
|
+
|
|
2369
|
+
Because all ECDFs are monotonic and start and end at the same
|
|
2370
|
+
points, the dot we put on the second-leftmost ECDF is also on the
|
|
2371
|
+
second curve from the top for some other x. Similarly, the
|
|
2372
|
+
second-rightmost ECDF is also on the second curve from the bottom
|
|
2373
|
+
for some other x. (You can sketch this out, and it becomes clear.)
|
|
2374
|
+
|
|
2375
|
+
So, any dot we put on an ECDF for computing a confidence interval
|
|
2376
|
+
for an IECDF is also a dot we would put on an ECDF for computing a
|
|
2377
|
+
confidence interval of the ECDF. If we want to compute the confidence
|
|
2378
|
+
interval over the whole domain of x-values, we will cover the same
|
|
2379
|
+
set of points if we compute the confidence interval of the ECDF or
|
|
2380
|
+
the IECDF. So, we end up filling between the same two sets of
|
|
2381
|
+
curves.
|
|
2382
|
+
|
|
2383
|
+
It turns out that the IECDF formulation is actually much easier to
|
|
2384
|
+
implement.
|
|
2385
|
+
"""
|
|
2386
|
+
data = utils._convert_data(data)
|
|
2387
|
+
|
|
2388
|
+
bs_reps = np.array(
|
|
2389
|
+
[np.sort(np.random.choice(data, size=len(data))) for _ in range(n_bs_reps)]
|
|
2390
|
+
)
|
|
2391
|
+
|
|
2392
|
+
# Compute the confidence intervals
|
|
2393
|
+
iecdf_low, iecdf_high = np.percentile(np.array(bs_reps), ptiles, axis=0)
|
|
2394
|
+
|
|
2395
|
+
# y-values for ECDFs
|
|
2396
|
+
y = np.arange(1, len(data) + 1) / len(data)
|
|
2397
|
+
|
|
2398
|
+
# Make them staircases
|
|
2399
|
+
x_low, y_plot = _to_staircase(x=iecdf_low, y=y)
|
|
2400
|
+
x_high, _ = _to_staircase(x=iecdf_high, y=y)
|
|
2401
|
+
|
|
2402
|
+
if q_axis == "y":
|
|
2403
|
+
if complementary:
|
|
2404
|
+
p, patch = utils._fill_between(
|
|
2405
|
+
p, x1=1 - y_plot, y1=x_low, x2=1 - y_plot, y2=x_high, **kwargs
|
|
2406
|
+
)
|
|
2407
|
+
else:
|
|
2408
|
+
p, patch = utils._fill_between(
|
|
2409
|
+
p, x1=y_plot, y1=x_low, x2=y_plot, y2=x_high, **kwargs
|
|
2410
|
+
)
|
|
2411
|
+
elif q_axis == "x":
|
|
2412
|
+
if complementary:
|
|
2413
|
+
p, patch = utils._fill_between(
|
|
2414
|
+
p, x1=x_low, y1=1 - y_plot, x2=x_high, y2=1 - y_plot, **kwargs
|
|
2415
|
+
)
|
|
2416
|
+
else:
|
|
2417
|
+
p, patch = utils._fill_between(
|
|
2418
|
+
p, x1=x_low, y1=y_plot, x2=x_high, y2=y_plot, **kwargs
|
|
2419
|
+
)
|
|
2420
|
+
else:
|
|
2421
|
+
raise RuntimeError("`q_axis` must be either 'x' or 'y'.")
|
|
2422
|
+
|
|
2423
|
+
return p, patch
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
def _ecdf_y(data, complementary=False):
|
|
2427
|
+
"""Give y-values of an ECDF for an unsorted column in a data frame.
|
|
2428
|
+
|
|
2429
|
+
Parameters
|
|
2430
|
+
----------
|
|
2431
|
+
data : Pandas Series
|
|
2432
|
+
Series (or column of a DataFrame) from which to generate ECDF
|
|
2433
|
+
values.
|
|
2434
|
+
complementary : bool, default False
|
|
2435
|
+
If True, give the ECCDF values.
|
|
2436
|
+
|
|
2437
|
+
Returns
|
|
2438
|
+
-------
|
|
2439
|
+
output : Pandas Series
|
|
2440
|
+
Corresponding y-values for an ECDF when plotted with dots.
|
|
2441
|
+
|
|
2442
|
+
Notes
|
|
2443
|
+
-----
|
|
2444
|
+
.. This only works for plotting an ECDF with points, not for formal
|
|
2445
|
+
or staircase ECDFs.
|
|
2446
|
+
"""
|
|
2447
|
+
if complementary:
|
|
2448
|
+
return 1 - data.rank(method="first") / len(data) + 1 / len(data)
|
|
2449
|
+
else:
|
|
2450
|
+
return data.rank(method="first") / len(data)
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
def _dist_legend(
|
|
2454
|
+
p,
|
|
2455
|
+
show_legend,
|
|
2456
|
+
legend_location,
|
|
2457
|
+
legend_orientation,
|
|
2458
|
+
click_policy,
|
|
2459
|
+
labels,
|
|
2460
|
+
markers,
|
|
2461
|
+
lines,
|
|
2462
|
+
patches,
|
|
2463
|
+
rays_high,
|
|
2464
|
+
rays_low,
|
|
2465
|
+
circles_high,
|
|
2466
|
+
circles_low,
|
|
2467
|
+
invisible_markers,
|
|
2468
|
+
extra_glyphs=None,
|
|
2469
|
+
):
|
|
2470
|
+
"""Add a legend to a histogram, spike, or ECDF plot.
|
|
2471
|
+
|
|
2472
|
+
`extra_glyphs` is an optional list with one additional glyph per
|
|
2473
|
+
label, e.g., the bin separators of a histogram. These are added to
|
|
2474
|
+
the legend item for the respective label so that they respond to
|
|
2475
|
+
clicks on the legend along with the rest of the glyphs.
|
|
2476
|
+
"""
|
|
2477
|
+
if show_legend:
|
|
2478
|
+
if len(markers) > 0:
|
|
2479
|
+
if len(lines) > 0:
|
|
2480
|
+
if len(patches) > 0:
|
|
2481
|
+
if len(invisible_markers) > 0:
|
|
2482
|
+
items = [
|
|
2483
|
+
(
|
|
2484
|
+
label,
|
|
2485
|
+
[
|
|
2486
|
+
line,
|
|
2487
|
+
patch,
|
|
2488
|
+
ray_high,
|
|
2489
|
+
ray_low,
|
|
2490
|
+
circle_high,
|
|
2491
|
+
circle_low,
|
|
2492
|
+
invisible_marker,
|
|
2493
|
+
marker,
|
|
2494
|
+
],
|
|
2495
|
+
)
|
|
2496
|
+
for label, line, patch, ray_high, ray_low, circle_high, circle_low, invisible_marker, marker in zip(
|
|
2497
|
+
labels,
|
|
2498
|
+
lines,
|
|
2499
|
+
patches,
|
|
2500
|
+
rays_high,
|
|
2501
|
+
rays_low,
|
|
2502
|
+
circles_high,
|
|
2503
|
+
circles_low,
|
|
2504
|
+
invisible_markers,
|
|
2505
|
+
markers,
|
|
2506
|
+
)
|
|
2507
|
+
]
|
|
2508
|
+
else:
|
|
2509
|
+
items = [
|
|
2510
|
+
(
|
|
2511
|
+
label,
|
|
2512
|
+
[
|
|
2513
|
+
line,
|
|
2514
|
+
patch,
|
|
2515
|
+
ray_high,
|
|
2516
|
+
ray_low,
|
|
2517
|
+
circle_high,
|
|
2518
|
+
circle_low,
|
|
2519
|
+
marker,
|
|
2520
|
+
],
|
|
2521
|
+
)
|
|
2522
|
+
for label, line, patch, ray_high, ray_low, circle_high, circle_low, marker in zip(
|
|
2523
|
+
labels,
|
|
2524
|
+
lines,
|
|
2525
|
+
patches,
|
|
2526
|
+
rays_high,
|
|
2527
|
+
rays_low,
|
|
2528
|
+
circles_high,
|
|
2529
|
+
circles_low,
|
|
2530
|
+
markers,
|
|
2531
|
+
)
|
|
2532
|
+
]
|
|
2533
|
+
else:
|
|
2534
|
+
if len(invisible_markers) > 0:
|
|
2535
|
+
items = [
|
|
2536
|
+
(
|
|
2537
|
+
label,
|
|
2538
|
+
[
|
|
2539
|
+
line,
|
|
2540
|
+
ray_high,
|
|
2541
|
+
ray_low,
|
|
2542
|
+
circle_high,
|
|
2543
|
+
circle_low,
|
|
2544
|
+
invisible_marker,
|
|
2545
|
+
marker,
|
|
2546
|
+
],
|
|
2547
|
+
)
|
|
2548
|
+
for label, line, ray_high, ray_low, circle_high, circle_low, invisible_marker, marker in zip(
|
|
2549
|
+
labels,
|
|
2550
|
+
lines,
|
|
2551
|
+
rays_high,
|
|
2552
|
+
rays_low,
|
|
2553
|
+
circles_high,
|
|
2554
|
+
circles_low,
|
|
2555
|
+
invisible_markers,
|
|
2556
|
+
markers,
|
|
2557
|
+
)
|
|
2558
|
+
]
|
|
2559
|
+
elif not rays_high:
|
|
2560
|
+
items = [
|
|
2561
|
+
(
|
|
2562
|
+
label,
|
|
2563
|
+
[
|
|
2564
|
+
line,
|
|
2565
|
+
marker,
|
|
2566
|
+
],
|
|
2567
|
+
)
|
|
2568
|
+
for label, line, marker in zip(
|
|
2569
|
+
labels,
|
|
2570
|
+
lines,
|
|
2571
|
+
markers,
|
|
2572
|
+
)
|
|
2573
|
+
]
|
|
2574
|
+
else:
|
|
2575
|
+
items = [
|
|
2576
|
+
(
|
|
2577
|
+
label,
|
|
2578
|
+
[
|
|
2579
|
+
line,
|
|
2580
|
+
ray_high,
|
|
2581
|
+
ray_low,
|
|
2582
|
+
circle_high,
|
|
2583
|
+
circle_low,
|
|
2584
|
+
marker,
|
|
2585
|
+
],
|
|
2586
|
+
)
|
|
2587
|
+
for label, line, ray_high, ray_low, circle_high, circle_low, marker in zip(
|
|
2588
|
+
labels,
|
|
2589
|
+
lines,
|
|
2590
|
+
rays_high,
|
|
2591
|
+
rays_low,
|
|
2592
|
+
circles_high,
|
|
2593
|
+
circles_low,
|
|
2594
|
+
markers,
|
|
2595
|
+
)
|
|
2596
|
+
]
|
|
2597
|
+
else:
|
|
2598
|
+
if len(patches) > 0:
|
|
2599
|
+
if len(invisible_markers) > 0:
|
|
2600
|
+
items = [
|
|
2601
|
+
(label, [marker, invisible_marker, patch])
|
|
2602
|
+
for label, marker, invisible_marker, patch in zip(
|
|
2603
|
+
labels, markers, invisible_markers, patches
|
|
2604
|
+
)
|
|
2605
|
+
]
|
|
2606
|
+
else:
|
|
2607
|
+
items = [
|
|
2608
|
+
(label, [marker, patch])
|
|
2609
|
+
for label, marker, patch in zip(labels, markers, patches)
|
|
2610
|
+
]
|
|
2611
|
+
else:
|
|
2612
|
+
items = [
|
|
2613
|
+
(label, [marker]) for label, marker in zip(labels, markers)
|
|
2614
|
+
]
|
|
2615
|
+
else:
|
|
2616
|
+
if len(patches) > 0:
|
|
2617
|
+
if len(rays_high) > 0:
|
|
2618
|
+
if len(invisible_markers) > 0:
|
|
2619
|
+
items = [
|
|
2620
|
+
(label, [line, patch, ray_high, ray_low, invisible_marker])
|
|
2621
|
+
for label, line, patch, ray_high, ray_low, invisible_marker in zip(
|
|
2622
|
+
labels,
|
|
2623
|
+
lines,
|
|
2624
|
+
patches,
|
|
2625
|
+
rays_high,
|
|
2626
|
+
rays_low,
|
|
2627
|
+
invisible_markers,
|
|
2628
|
+
)
|
|
2629
|
+
]
|
|
2630
|
+
else:
|
|
2631
|
+
items = [
|
|
2632
|
+
(label, [line, patch, ray_high, ray_low])
|
|
2633
|
+
for label, line, patch, ray_high, ray_low in zip(
|
|
2634
|
+
labels, lines, patches, rays_high, rays_low
|
|
2635
|
+
)
|
|
2636
|
+
]
|
|
2637
|
+
else:
|
|
2638
|
+
if len(invisible_markers) > 0:
|
|
2639
|
+
items = [
|
|
2640
|
+
(label, [line, patch, invisible_marker])
|
|
2641
|
+
for label, line, patch, invisible_marker in zip(
|
|
2642
|
+
labels, lines, patches, invisible_markers
|
|
2643
|
+
)
|
|
2644
|
+
]
|
|
2645
|
+
else:
|
|
2646
|
+
items = [
|
|
2647
|
+
(label, [line, patch])
|
|
2648
|
+
for label, line, patch in zip(labels, lines, patches)
|
|
2649
|
+
]
|
|
2650
|
+
else:
|
|
2651
|
+
if len(rays_high) > 0:
|
|
2652
|
+
if len(invisible_markers) > 0:
|
|
2653
|
+
items = [
|
|
2654
|
+
(label, [line, ray_high, ray_low, invisible_marker])
|
|
2655
|
+
for label, line, ray_high, ray_low, invisible_marker in zip(
|
|
2656
|
+
labels, lines, rays_high, rays_low, invisible_markers
|
|
2657
|
+
)
|
|
2658
|
+
]
|
|
2659
|
+
else:
|
|
2660
|
+
items = [
|
|
2661
|
+
(label, [line, ray_high, ray_low])
|
|
2662
|
+
for label, line, ray_high, ray_low in zip(
|
|
2663
|
+
labels, lines, rays_high, rays_low
|
|
2664
|
+
)
|
|
2665
|
+
]
|
|
2666
|
+
else:
|
|
2667
|
+
if len(invisible_markers) > 0:
|
|
2668
|
+
items = [
|
|
2669
|
+
(label, [line, invisible_marker])
|
|
2670
|
+
for label, line, invisible_marker in zip(
|
|
2671
|
+
labels, lines, invisible_markers
|
|
2672
|
+
)
|
|
2673
|
+
]
|
|
2674
|
+
else:
|
|
2675
|
+
items = [(label, [line]) for label, line in zip(labels, lines)]
|
|
2676
|
+
|
|
2677
|
+
if extra_glyphs is not None and len(extra_glyphs) > 0:
|
|
2678
|
+
items = [
|
|
2679
|
+
(label, glyphs + [extra_glyph])
|
|
2680
|
+
for (label, glyphs), extra_glyph in zip(items, extra_glyphs)
|
|
2681
|
+
]
|
|
2682
|
+
|
|
2683
|
+
if len(p.legend) == 1:
|
|
2684
|
+
for item in items:
|
|
2685
|
+
p.legend.items.append(
|
|
2686
|
+
bokeh.models.LegendItem(label=item[0], renderers=item[1])
|
|
2687
|
+
)
|
|
2688
|
+
else:
|
|
2689
|
+
if len(p.legend) > 1:
|
|
2690
|
+
warnings.warn(
|
|
2691
|
+
"Ambiguous which legend to add glyphs to. Creating new legend."
|
|
2692
|
+
)
|
|
2693
|
+
if legend_location in ["right", "left", "above", "below"]:
|
|
2694
|
+
legend = bokeh.models.Legend(
|
|
2695
|
+
items=items, location="center", orientation=legend_orientation
|
|
2696
|
+
)
|
|
2697
|
+
p.add_layout(legend, legend_location)
|
|
2698
|
+
elif legend_location in [
|
|
2699
|
+
"top_left",
|
|
2700
|
+
"top_center",
|
|
2701
|
+
"top_right",
|
|
2702
|
+
"center_right",
|
|
2703
|
+
"bottom_right",
|
|
2704
|
+
"bottom_center",
|
|
2705
|
+
"bottom_left",
|
|
2706
|
+
"center_left",
|
|
2707
|
+
"center",
|
|
2708
|
+
] or isinstance(legend_location, tuple):
|
|
2709
|
+
legend = bokeh.models.Legend(
|
|
2710
|
+
items=items,
|
|
2711
|
+
location=legend_location,
|
|
2712
|
+
orientation=legend_orientation,
|
|
2713
|
+
)
|
|
2714
|
+
p.add_layout(legend, "center")
|
|
2715
|
+
else:
|
|
2716
|
+
raise RuntimeError(
|
|
2717
|
+
'Invalid `legend_location`. Must be a 2-tuple specifying location or one of ["right", "left", "above", "below", "top_left", "top_center", "top_right", "center_right", "bottom_right", "bottom_center", "bottom_left", "center_left", "center"]'
|
|
2718
|
+
)
|
|
2719
|
+
|
|
2720
|
+
p.legend.click_policy = click_policy
|
|
2721
|
+
|
|
2722
|
+
return p
|
|
2723
|
+
|
|
2724
|
+
|
|
2725
|
+
def _sort_df(df, cats, order):
|
|
2726
|
+
if order is not None and cats is not None:
|
|
2727
|
+
if isinstance(cats, (list, tuple)):
|
|
2728
|
+
df["__sort"] = df.apply(lambda r: order.index(tuple(r[cats])), axis=1)
|
|
2729
|
+
else:
|
|
2730
|
+
df["__sort"] = df.apply(lambda r: order.index(r[cats]), axis=1)
|
|
2731
|
+
df = df.sort_values(by="__sort")
|
|
2732
|
+
|
|
2733
|
+
return df
|
|
2734
|
+
|
|
2735
|
+
|
|
2736
|
+
def _compute_histogram(data, bins, density):
|
|
2737
|
+
"""Computes the bins and edges of a histogram."""
|
|
2738
|
+
if isinstance(bins, str) and bins == "sqrt":
|
|
2739
|
+
bins = int(np.ceil(np.sqrt(len(data))))
|
|
2740
|
+
elif isinstance(bins, str) and bins == "freedman-diaconis":
|
|
2741
|
+
h = 2 * (np.percentile(data, 75) - np.percentile(data, 25)) / np.cbrt(len(data))
|
|
2742
|
+
if h == 0.0:
|
|
2743
|
+
bins = 3
|
|
2744
|
+
else:
|
|
2745
|
+
bins = int(np.ceil((data.max() - data.min()) / h))
|
|
2746
|
+
|
|
2747
|
+
f, e = np.histogram(data, bins=bins, density=density)
|
|
2748
|
+
|
|
2749
|
+
return bins, e, f
|
|
2750
|
+
|
|
2751
|
+
|
|
2752
|
+
def _hist_for_plotting(e, f):
|
|
2753
|
+
"""Takes output e and f from _compute_histogram(), and generates
|
|
2754
|
+
x, y values for plotting the histogram."""
|
|
2755
|
+
e0 = np.empty(2 * len(e))
|
|
2756
|
+
f0 = np.empty(2 * len(e))
|
|
2757
|
+
e0[::2] = e
|
|
2758
|
+
e0[1::2] = e
|
|
2759
|
+
f0[0] = 0
|
|
2760
|
+
f0[-1] = 0
|
|
2761
|
+
f0[1:-1:2] = f
|
|
2762
|
+
f0[2:-1:2] = f
|
|
2763
|
+
|
|
2764
|
+
return e0, f0
|