Skip to content

Commit

Permalink
implement axes logscales (handley-lab#328)
Browse files Browse the repository at this point in the history
* add logscale capability to `kde_contour_plot_2d`

* add logscale capability to `fastkde_contour_plot_2d`, and remove `set_xlim` which should really be determined by the contourplots

* add logscale capability to `hist_plot_2d`

* presumably fix `test_hist_plot_2d` which did not create new figures despite checking for axes limits

* add logscale capability to `kde_plot_1d`

* add logscale capability to `fastkde_plot_1d`

* add `logx` and `logxy` kwargs to `make_1d_axes`, `make_2d_axes`, `plot_1d`, and `plot_2d` which takes a list of parameters that are to be plotted on a log-scale

* add logscale capability to `hist_plot_1d`

* fix `logx` and `logxy` behaviour to work for empty lists and both for already existing axes and newly to create axes

* add log-scale capability to `hist_plot_1d` when it is called on its own

* add tests for log-scale capability of the anesthetic plotting functions

* attempt setting logscale only if ax not None

* move `local_kwargs` instantiation until after `logxy` is popped from kwargs

* add test for logscale creation in `make_1d_axes` and `make_2d_axes`

* add test for correct logscale handling of `Samples.plot_1d` and `Samples.plot_2d`

* version bump to 2.2.0

* skip logscale tests for fastkde if not installed

* add `logx`, `logxy`, and `label` to the docstring of `plot_1d` and/or `plot_2d`

* add documentation for log-scale usage

* add test checking for ValueError if log-axes do not match in repeated calls to `plot_1d` or `plot_2d`

* add minimum requirement of `Sphinx>=4.2.0`

* add minimum requirement of `sphinx_rtd_theme>=1.2.2`

* version bump to 2.3.0

* version bump to 2.4.0

* fix logscaling post master merge

* add test for combination of logscale hist plot with bins and range kwargs

* add test for combination of logscale hist plot with bins and range kwargs in test_samples

* fix `hist_plot_1d` for various combinations of `bins` and `range` kwargs with logscale, and remove astropy option since we now have string input for bins independent of astropy

* add `noqa: disable=D101` to suppress `missing docstring`

* change tests involving astropy to now test for ValueError, since astropy has been removed, since automatic bin computation is now directly integrated in anesthetic

* change `logxy` to independent `logx` and `logy`

* adjust docs to new `logx` and `logy` kwargs instead of the `logxy` kwarg

* change docs on logscale to plot more parameters to see the different `logx` and `logy` behaviour

* change docs on logscale to include descriptive legends

* version bump to 2.5.0

* add pytest warning capture for hist

* replace allsegs with get_paths

* fix range of hist1d as data has already been logarithmed

* get_paths only works on matplotlib>=3.8.0

* split logscale tests up for 1d and 2d, to make it easier to identify where issues reside

* do not emit to orthogonal axes when logx and logy are different, and use data bounds for fastkde by default

* revert the data constrained bounds from fastkde, they help with sharp bounds, but they ruin Gaussian results, this is very different from gaussian_kde, so for fastkde we actually would need to provide prior bounds and it is not good enough to infer bounds from the data...

* add test for setting limits when horizontal axes are linear and vertical are logarithmic or vice versa, where those limits should not emit

* simplify tests from previous commit to focus only on the cases that could actually fail

* make new tests compatible with python 3.9

* clean up warnings import setup

---------

Co-authored-by: AdamOrmondroyd <[email protected]>
  • Loading branch information
lukashergt and AdamOrmondroyd authored Nov 2, 2023
1 parent 2c01d13 commit 10f032f
Show file tree
Hide file tree
Showing 8 changed files with 580 additions and 76 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
anesthetic: nested sampling post-processing
===========================================
:Authors: Will Handley and Lukas Hergt
:Version: 2.4.2
:Version: 2.5.0
:Homepage: https://github.com/handley-lab/anesthetic
:Documentation: http://anesthetic.readthedocs.io/

Expand Down
2 changes: 1 addition & 1 deletion anesthetic/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.4.2'
__version__ = '2.5.0'
182 changes: 141 additions & 41 deletions anesthetic/plot.py

Large diffs are not rendered by default.

61 changes: 44 additions & 17 deletions anesthetic/plotting/_matplotlib/hist.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,24 @@


class HistPlot(_WeightedMPLPlot, _HistPlot):

# noqa: disable=D101
def _args_adjust(self) -> None:
if (
hasattr(self, 'bins') and
isinstance(self.bins, str) and
self.bins in ['fd', 'scott', 'sqrt']
):
self.bins = self._calculate_bins(self.data)
super()._args_adjust()

# noqa: disable=D101
def _calculate_bins(self, data):
if self.logx:
data = np.log10(data)
if 'range' in self.kwds and self.kwds['range'] is not None:
xmin, xmax = self.kwds['range']
self.kwds['range'] = (np.log10(xmin), np.log10(xmax))
nd_values = data.infer_objects(copy=False)._get_numeric_data()
values = np.ravel(nd_values)
weights = self.kwds.get("weights", None)
Expand All @@ -41,10 +57,25 @@ def _calculate_bins(self, data):

values = values[~isna(values)]

hist, bins = np.histogram(
values, bins=self.bins, range=self.kwds.get("range", None),
weights=weights
)
if isinstance(self.bins, str) and self.bins in ['fd', 'scott', 'sqrt']:
bins = histogram_bin_edges(
values,
weights=weights,
bins=self.bins,
beta=self.kwds.pop('beta', 'equal'),
range=self.kwds.get('range', None)
)
else:
bins = np.histogram_bin_edges(
values,
weights=weights,
bins=self.bins,
range=self.kwds.get('range', None)
)
if self.logx:
bins = 10**bins
if 'range' in self.kwds and self.kwds['range'] is not None:
self.kwds['range'] = (xmin, xmax)
return bins

def _get_colors(self, num_colors=None, color_kwds='color'):
Expand Down Expand Up @@ -149,23 +180,19 @@ def __init__(
) -> None:
super().__init__(data, bins=bins, bottom=bottom, **kwargs)

def _args_adjust(self) -> None:
if 'range' not in self.kwds:
def _calculate_bins(self, data):
if 'range' not in self.kwds or self.kwds['range'] is None:
q = self.kwds.get('q', 5)
q = quantile_plot_interval(q=q)
weights = self.kwds.get('weights', None)
xmin = quantile(self.data, q[0], weights)
xmax = quantile(self.data, q[-1], weights)
xmin = quantile(data, q[0], weights)
xmax = quantile(data, q[-1], weights)
self.kwds['range'] = (xmin, xmax)
if isinstance(self.bins, str) and self.bins in ['fd', 'scott', 'sqrt']:
self.bins = histogram_bin_edges(
self.data,
weights=self.kwds.get('weights', None),
bins=self.bins,
beta=self.kwds.pop('beta', 'equal'),
range=self.kwds.get('range', None)
)
super()._args_adjust()
bins = super()._calculate_bins(data)
self.kwds.pop('range')
else:
bins = super()._calculate_bins(data)
return bins

@classmethod
def _plot(
Expand Down
55 changes: 45 additions & 10 deletions anesthetic/samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,13 @@ def plot_1d(self, axes=None, *args, **kwargs):
can be hard to interpret/expensive for :class:`Samples`,
:class:`MCMCSamples`, or :class:`NestedSamples`.
logx : list(str), optional
Which parameters/columns to plot on a log scale.
Needs to match if plotting on top of a pre-existing axes.
label : str, optional
Legend label added to each axis.
Returns
-------
axes : :class:`pandas.Series` of :class:`matplotlib.axes.Axes`
Expand All @@ -215,7 +222,14 @@ def plot_1d(self, axes=None, *args, **kwargs):
axes = self.drop_labels().columns

if not isinstance(axes, AxesSeries):
_, axes = make_1d_axes(axes, labels=self.get_labels_map())
_, axes = make_1d_axes(axes, labels=self.get_labels_map(),
logx=kwargs.pop('logx', None))
logx = axes._logx
else:
logx = kwargs.pop('logx', axes._logx)
if logx != axes._logx:
raise ValueError(f"logx does not match the pre-existing axes."
f"logx={logx}, axes._logx={axes._logx}")

kwargs['kind'] = kwargs.get('kind', 'kde_1d')
kwargs['label'] = kwargs.get('label', self.label)
Expand All @@ -237,7 +251,7 @@ def plot_1d(self, axes=None, *args, **kwargs):
for x, ax in axes.items():
if x in self and kwargs['kind'] is not None:
xlabel = self.get_label(x)
self[x].plot(ax=ax, xlabel=xlabel,
self[x].plot(ax=ax, xlabel=xlabel, logx=x in logx,
*args, **kwargs)
ax.set_xlabel(xlabel)
else:
Expand Down Expand Up @@ -314,6 +328,14 @@ def plot_2d(self, axes=None, *args, **kwargs):
overwrite any kwarg with the same key passed to <sub>_kwargs.
Default: {}
logx, logy : list(str), optional
Which parameters/columns to plot on a log scale for the x-axis and
y-axis, respectively.
Needs to match if plotting on top of a pre-existing axes.
label : str, optional
Legend label added to each axis.
Returns
-------
axes : :class:`pandas.DataFrame` of :class:`matplotlib.axes.Axes`
Expand Down Expand Up @@ -341,21 +363,32 @@ def plot_2d(self, axes=None, *args, **kwargs):
"the following string shortcuts: "
f"{list(self.plot_2d_default_kinds.keys())}")

local_kwargs = {pos: kwargs.pop('%s_kwargs' % pos, {})
for pos in ['upper', 'lower', 'diagonal']}
kwargs['label'] = kwargs.get('label', self.label)

for pos in local_kwargs:
local_kwargs[pos].update(kwargs)

if axes is None:
axes = self.drop_labels().columns

if not isinstance(axes, AxesDataFrame):
_, axes = make_2d_axes(axes, labels=self.get_labels_map(),
upper=('upper' in kind),
lower=('lower' in kind),
diagonal=('diagonal' in kind))
diagonal=('diagonal' in kind),
logx=kwargs.pop('logx', None),
logy=kwargs.pop('logy', None))
logx = axes._logx
logy = axes._logy
else:
logx = kwargs.pop('logx', axes._logx)
logy = kwargs.pop('logy', axes._logy)
if logx != axes._logx or logy != axes._logy:
raise ValueError(f"logx or logy not matching existing axes:"
f"logx={logx}, axes._logx={axes._logx}"
f"logy={logy}, axes._logy={axes._logy}")

local_kwargs = {pos: kwargs.pop('%s_kwargs' % pos, {})
for pos in ['upper', 'lower', 'diagonal']}
kwargs['label'] = kwargs.get('label', self.label)

for pos in local_kwargs:
local_kwargs[pos].update(kwargs)

for y, row in axes.iterrows():
for x, ax in row.items():
Expand Down Expand Up @@ -383,11 +416,13 @@ def plot_2d(self, axes=None, *args, **kwargs):
ylabel = self.get_label(y)
if x == y:
self[x].plot(ax=ax.twin, xlabel=xlabel,
logx=x in logx,
*args, **lkwargs)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
else:
self.plot(x, y, ax=ax, xlabel=xlabel,
logx=x in logx, logy=y in logy,
ylabel=ylabel, *args, **lkwargs)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
Expand Down
29 changes: 28 additions & 1 deletion docs/source/plotting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,37 @@ method from there, directing it to the correct position with the ``loc`` and
axes.iloc[ 0, -1].legend(loc='lower right', bbox_to_anchor=(1, 1))
axes.iloc[-1, 0].legend(loc='lower center', bbox_to_anchor=(len(axes)/2, len(axes)))

Log-scale
---------

You can plot selected parameters on a log-scale by passing a list of those
parameters under the keyword ``logx`` to :func:`anesthetic.plot.make_1d_axes`
or :meth:`anesthetic.samples.Samples.plot_1d`, and under the keywords ``logx``
and ``logy`` to :func:`anesthetic.plot.make_2d_axes` or
:meth:`anesthetic.samples.Samples.plot_2d`:

.. plot:: :context: close-figs

fig, axes = make_1d_axes(['x0', 'x1', 'x2', 'x3'], logx=['x2'])
samples.plot_1d(axes, label="'x2' on log-scale")
axes['x2'].legend()

.. plot:: :context: close-figs

fig, axes = make_2d_axes(['x0', 'x1', 'x2', 'x3'], logx=['x2'], logy=['x2'])
samples.plot_2d(axes, label="'x2' on log-scale")
axes.iloc[-1, 0].legend(bbox_to_anchor=(len(axes), len(axes)), loc='lower right')

.. plot:: :context: close-figs

fig, axes = make_2d_axes(['x0', 'x1', 'x2', 'x3'], logx=['x2'])
samples.plot_2d(axes, label="'x2' on log-scale for x-axis, but not for y-axis")
axes.iloc[-1, 0].legend(bbox_to_anchor=(len(axes), len(axes)), loc='lower right')

Ticks
-----

You can pass the keyword ``ticks`` to :func:anesthetic.plot.make_2d_axes: to
You can pass the keyword ``ticks`` to :func:`anesthetic.plot.make_2d_axes`: to
adjust the tick settings of the 2D axes. There are three options:

* ``ticks='inner'``
Expand Down
Loading

0 comments on commit 10f032f

Please sign in to comment.