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/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