Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add circular histogram and KDE plot #1266

Merged
merged 26 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6dc14ca
add circular histogram and KDE plot
agustinaarroyuelo Jun 25, 2020
f82a765
add circular histogram using bokeh
agustinaarroyuelo Jun 26, 2020
dab16c1
fix bin computation
agustinaarroyuelo Jun 29, 2020
f811169
fix overlapping bins
agustinaarroyuelo Jul 1, 2020
599825c
update changelog
agustinaarroyuelo Jul 1, 2020
b1be37c
fix errors on plot_energy
agustinaarroyuelo Jul 1, 2020
8f02d64
update CHANGELOG.md
agustinaarroyuelo Jul 1, 2020
2f5b77d
several fixes
agustinaarroyuelo Jul 3, 2020
ea8859f
define tick placement function
agustinaarroyuelo Jul 3, 2020
e8db4dd
update docstring
agustinaarroyuelo Jul 6, 2020
f41056f
update docstring
agustinaarroyuelo Jul 6, 2020
680f36f
update tests mpl
agustinaarroyuelo Jul 6, 2020
506048e
run black
agustinaarroyuelo Jul 6, 2020
5793399
update hist_kwargs
agustinaarroyuelo Jul 7, 2020
6020607
add test for plot_utils function
agustinaarroyuelo Jul 7, 2020
3c7670a
use importorskip for testing bokeh function
agustinaarroyuelo Jul 7, 2020
3c42a3a
fix test
agustinaarroyuelo Jul 7, 2020
5b0a0a4
align mpl histogram bars to center
agustinaarroyuelo Jul 7, 2020
8a99512
fix bokeh wedge alignment
agustinaarroyuelo Jul 7, 2020
5bf3064
update ylim
agustinaarroyuelo Jul 11, 2020
97cdaca
update kdeplot docstring
agustinaarroyuelo Jul 11, 2020
4fd0f5c
fix yticks
agustinaarroyuelo Jul 11, 2020
42aa26a
add skipif in test_plot_utils
agustinaarroyuelo Jul 11, 2020
d97e1e0
update docstring
agustinaarroyuelo Jul 11, 2020
b6e50f1
change yticks
agustinaarroyuelo Jul 11, 2020
583079c
update tests
agustinaarroyuelo Jul 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## v0.x.x Unreleased
### New features
* Added InferenceData dataset containing circular variables (#1265)

* Added `is_circular` argument to `plot_dist` and `plot_kde` allowing for a circular histogram (Matplotlib, Bokeh) or 1D KDE plot (Matplotlib). (#1266)
### Maintenance and fixes
* plot_posterior: fix overlap of hdi and rope (#1263)

Expand Down Expand Up @@ -73,7 +73,7 @@

### New features
* Stats and plotting functions that provide `var_names` arg can now filter parameters based on partial naming (`filter="like"`) or regular expressions (`filter="regex"`) (see [#1154](https://github.com/arviz-devs/arviz/pull/1154)).
* Add `true_values` argument for `plot_pair`. It allows for a scatter plot showing the true values of the variables #1140
* Add `true_values` argument for `plot_pair`. It allows for a scatter plot showing the true values of the variables (#1140)
* Allow xarray.Dataarray input for plots.(#1120)
* Revamped the `hpd` function to make it work with mutidimensional arrays, InferenceData and xarray objects (#1117)
* Skip test for optional/extra dependencies when not installed (#1113)
Expand Down
61 changes: 55 additions & 6 deletions arviz/plots/backends/bokeh/distplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from . import backend_kwarg_defaults
from .. import show_layout
from ...kdeplot import plot_kde
from ...plot_utils import set_bokeh_circular_ticks_labels
from ....numeric_utils import get_bins


Expand All @@ -29,6 +30,7 @@ def plot_dist(
contourf_kwargs,
pcolormesh_kwargs,
hist_kwargs,
is_circular,
ax,
backend_kwargs,
show,
Expand All @@ -43,14 +45,22 @@ def plot_dist(
**backend_kwargs,
}
if ax is None:
ax = bkp.figure(**backend_kwargs)
if is_circular:
ax = bkp.figure(x_axis_type=None, y_axis_type=None)
else:
ax = bkp.figure(**backend_kwargs)

if kind == "auto":
kind = "hist" if values.dtype.kind == "i" else "kde"

if kind == "hist":
_histplot_bokeh_op(
values=values, values2=values2, rotated=rotated, ax=ax, hist_kwargs=hist_kwargs
values=values,
values2=values2,
rotated=rotated,
ax=ax,
hist_kwargs=hist_kwargs,
is_circular=is_circular,
)
elif kind == "kde":
if plot_kwargs is None:
Expand Down Expand Up @@ -91,7 +101,7 @@ def plot_dist(
return ax


def _histplot_bokeh_op(values, values2, rotated, ax, hist_kwargs):
def _histplot_bokeh_op(values, values2, rotated, ax, hist_kwargs, is_circular):
"""Add a histogram for the data to the axes."""
if values2 is not None:
raise NotImplementedError("Insert hexbin plot here")
Expand All @@ -105,6 +115,8 @@ def _histplot_bokeh_op(values, values2, rotated, ax, hist_kwargs):
hist_kwargs["fill_color"] = color
hist_kwargs["line_color"] = color

hist_kwargs.setdefault("line_alpha", 0)

# remove defaults for mpl
hist_kwargs.pop("rwidth", None)
hist_kwargs.pop("align", None)
Expand All @@ -119,8 +131,45 @@ def _histplot_bokeh_op(values, values2, rotated, ax, hist_kwargs):
if hist_kwargs.pop("cumulative", False):
hist = np.cumsum(hist)
hist /= hist[-1]
if rotated:
ax.quad(top=edges[:-1], bottom=edges[1:], left=0, right=hist, **hist_kwargs)

if is_circular:

if is_circular == "degrees":
edges = np.deg2rad(edges)
labels = ["0°", "45°", "90°", "135°", "180°", "225°", "270°", "315°"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are always showing a full circle plot?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's what everybody does (I know this is not a very good argument, haha)

else:

labels = [
r"0",
r"π/4",
r"π/2",
r"3π/4",
r"π",
r"5π/4",
r"3π/2",
r"7π/4",
]

delta = np.mean(np.diff(edges) / 2)

ax.annular_wedge(
x=0,
y=0,
inner_radius=0,
outer_radius=hist,
start_angle=edges[1:] - delta,
end_angle=edges[:-1] - delta,
direction="clock",
**hist_kwargs,
)

ax = set_bokeh_circular_ticks_labels(ax, hist, labels)

else:
ax.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], **hist_kwargs)

if rotated:
ax.quad(top=edges[:-1], bottom=edges[1:], left=0, right=hist, **hist_kwargs)
else:
ax.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], **hist_kwargs)

return ax
7 changes: 6 additions & 1 deletion arviz/plots/backends/bokeh/energyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ def plot_energy(
hist_kwargs["line_color"] = None
hist_kwargs["line_alpha"] = alpha
_histplot_bokeh_op(
value.flatten(), values2=None, rotated=False, ax=ax, hist_kwargs=hist_kwargs
value.flatten(),
values2=None,
rotated=False,
ax=ax,
hist_kwargs=hist_kwargs,
is_circular=False,
)

else:
Expand Down
48 changes: 42 additions & 6 deletions arviz/plots/backends/matplotlib/distplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def plot_dist(
contourf_kwargs,
pcolormesh_kwargs,
hist_kwargs,
is_circular,
ax,
backend_kwargs,
show,
Expand All @@ -43,11 +44,16 @@ def plot_dist(
)
backend_kwargs = None
if ax is None:
ax = plt.gca()
ax = plt.gca(polar=is_circular)
Comment on lines 46 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using gca instead of a new axes intended or a forgotten TODO from when bokeh was introduced? cc @ahartikainen @canyon289 @aloctavodia

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is intended

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will gca grab possibly old axis here? Have we created a new figure somewhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if present, plt.gca will grab old axes yes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is considered a "best practice" with matplotlib, and a pattern they endorse.


if kind == "hist":
ax = _histplot_mpl_op(
values=values, values2=values2, rotated=rotated, ax=ax, hist_kwargs=hist_kwargs
values=values,
values2=values2,
rotated=rotated,
ax=ax,
hist_kwargs=hist_kwargs,
is_circular=is_circular,
)

elif kind == "kde":
Expand Down Expand Up @@ -77,6 +83,7 @@ def plot_dist(
ax=ax,
backend="matplotlib",
backend_kwargs=backend_kwargs,
is_circular=is_circular,
show=show,
)

Expand All @@ -86,20 +93,49 @@ def plot_dist(
return ax


def _histplot_mpl_op(values, values2, rotated, ax, hist_kwargs):
def _histplot_mpl_op(values, values2, rotated, ax, hist_kwargs, is_circular):
"""Add a histogram for the data to the axes."""
bins = hist_kwargs.pop("bins", None)

if is_circular == "degrees":
if bins is None:
bins = get_bins(values)
values = np.deg2rad(values)
bins = np.deg2rad(bins)
ax.set_yticklabels([])

elif is_circular:
labels = [
r"0",
r"π/4",
r"π/2",
r"3π/4",
r"π",
r"5π/4",
r"3π/2",
r"7π/4",
]

ax.set_xticklabels(labels)
ax.set_yticklabels([])

if values2 is not None:
raise NotImplementedError("Insert hexbin plot here")

bins = hist_kwargs.pop("bins")
if bins is None:
bins = get_bins(values)
ax.hist(np.asarray(values).flatten(), bins=bins, **hist_kwargs)

if rotated:
n, _, _ = ax.hist(np.asarray(values).flatten(), bins=bins, **hist_kwargs)

if rotated or is_circular:
ax.set_yticks(bins[:-1])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In polar plots, isn't the y axis the radial one? could this be setting the radial ticks with x values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for spotting this!

else:
ax.set_xticks(bins[:-1])

if is_circular:
ax.set_ylim(0, n.max() + 0.5 * n.max())
agustinaarroyuelo marked this conversation as resolved.
Show resolved Hide resolved

if hist_kwargs.get("label") is not None:
ax.legend()

return ax
23 changes: 22 additions & 1 deletion arviz/plots/backends/matplotlib/kdeplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def plot_kde(
contour_kwargs,
contourf_kwargs,
pcolormesh_kwargs,
is_circular,
ax,
legend,
backend_kwargs,
Expand Down Expand Up @@ -76,7 +77,27 @@ def plot_kde(

rug_space = max(density) * rug_kwargs.pop("space")

x = np.linspace(lower, upper, len(density))
if is_circular:

if is_circular == "radians":
labels = [
r"0",
r"π/4",
r"π/2",
r"3π/4",
r"π",
r"5π/4",
r"3π/2",
r"7π/4",
]

ax.set_xticklabels(labels)

x = np.linspace(-np.pi, np.pi, len(density))
ax.set_yticklabels([])

else:
x = np.linspace(lower, upper, len(density))

fill_func = ax.fill_between
fill_x, fill_y = x, density
Expand Down
7 changes: 5 additions & 2 deletions arviz/plots/distplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import xarray as xr
from .plot_utils import get_plotting_function, matplotlib_kwarg_dealiaser
from ..numeric_utils import get_bins
from ..data import InferenceData
from ..rcparams import rcParams

Expand All @@ -29,6 +28,7 @@ def plot_dist(
contourf_kwargs=None,
pcolormesh_kwargs=None,
hist_kwargs=None,
is_circular=False,
ax=None,
backend=None,
backend_kwargs=None,
Expand Down Expand Up @@ -90,6 +90,9 @@ def plot_dist(
Keywords passed to ax.pcolormesh. Ignored for 1D KDE.
hist_kwargs : dict
Keywords passed to the histogram.
is_circular : {False, True, "radians", "degrees"}. Default False.
Select input type {"radians", "degrees"} for circular histogram or KDE plot. If True,
default input type is "radians".
ax: axes, optional
Matplotlib axes or bokeh figures.
backend: str, optional
Expand Down Expand Up @@ -160,7 +163,6 @@ def plot_dist(

if kind == "hist":
hist_kwargs = matplotlib_kwarg_dealiaser(hist_kwargs, "hist")
hist_kwargs.setdefault("bins", get_bins(values))
hist_kwargs.setdefault("cumulative", cumulative)
hist_kwargs.setdefault("color", color)
hist_kwargs.setdefault("label", label)
Expand Down Expand Up @@ -197,6 +199,7 @@ def plot_dist(
hist_kwargs=hist_kwargs,
ax=ax,
backend_kwargs=backend_kwargs,
is_circular=is_circular,
show=show,
**kwargs,
)
Expand Down
5 changes: 5 additions & 0 deletions arviz/plots/kdeplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def plot_kde(
contour_kwargs=None,
contourf_kwargs=None,
pcolormesh_kwargs=None,
is_circular=False,
ax=None,
legend=True,
backend=None,
Expand Down Expand Up @@ -81,6 +82,8 @@ def plot_kde(
Keywords passed to ax.contourf to draw filled contours. Ignored for 1D KDE.
pcolormesh_kwargs : dict
Keywords passed to ax.pcolormesh. Ignored for 1D KDE.
is_circular : str
Select input type {"radians","degrees"} for circular histogram or KDE plot.
agustinaarroyuelo marked this conversation as resolved.
Show resolved Hide resolved
ax: axes, optional
Matplotlib axes or bokeh figures.
legend : bool
Expand Down Expand Up @@ -230,6 +233,7 @@ def plot_kde(
contour_kwargs=contour_kwargs,
contourf_kwargs=contourf_kwargs,
pcolormesh_kwargs=pcolormesh_kwargs,
is_circular=is_circular,
ax=ax,
legend=legend,
backend_kwargs=backend_kwargs,
Expand All @@ -246,6 +250,7 @@ def plot_kde(
kde_plot_args.pop("textsize")
kde_plot_args.pop("label")
kde_plot_args.pop("legend")
kde_plot_args.pop("is_circular")
else:
kde_plot_args.pop("return_glyph")

Expand Down
48 changes: 48 additions & 0 deletions arviz/plots/plot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,51 @@ def sample_reference_distribution(dist, shape):
x_ss.append(x_s)
densities.append(density)
return np.array(x_ss).T, np.array(densities).T


def set_bokeh_circular_ticks_labels(ax, hist, labels):
"""Place ticks and ticklabels on Bokeh's circular histogram."""
ticks = np.linspace(-np.pi, np.pi, len(labels), endpoint=False)
ax.annular_wedge(
x=0,
y=0,
inner_radius=0,
outer_radius=np.max(hist) * 1.1,
start_angle=ticks,
end_angle=ticks,
line_color="grey",
)

radii_circles = np.linspace(0, np.max(hist) * 1.1, 4)
ax.circle(0, 0, radius=radii_circles, fill_color=None, line_color="grey")

offset = np.max(hist * 1.05) * 0.15
ticks_labels_pos_1 = np.max(hist * 1.05)
ticks_labels_pos_2 = ticks_labels_pos_1 * np.sqrt(2) / 2

ax.text(
[
ticks_labels_pos_1 + offset,
ticks_labels_pos_2 + offset,
0,
-ticks_labels_pos_2 - offset,
-ticks_labels_pos_1 - offset,
-ticks_labels_pos_2 - offset,
0,
ticks_labels_pos_2 + offset,
],
[
0,
ticks_labels_pos_2 + offset / 2,
ticks_labels_pos_1 + offset,
ticks_labels_pos_2 + offset / 2,
0,
-ticks_labels_pos_2 - offset,
-ticks_labels_pos_1 - offset,
-ticks_labels_pos_2 - offset,
],
text=labels,
text_align="center",
)

return ax
Loading