Commit 4090fc53 authored by Leo Pound Singer's avatar Leo Pound Singer
Browse files

Add PPPlot class

Original: ef4f7af18db31b9eba810f177c666aab790af2b2
parent 267092bc
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
*.log *.log
*.o *.o
*.pc *.pc
*.pyc
.deps .deps
.libs .libs
/_build/ /_build/
...@@ -55,4 +56,5 @@ test/LALInferencePriorTest ...@@ -55,4 +56,5 @@ test/LALInferencePriorTest
test/LALInferenceProposalTest test/LALInferenceProposalTest
test/LALInferenceTest test/LALInferenceTest
test/LALInferenceXMLTest test/LALInferenceXMLTest
test/test_result_images
test/test_vot.xml test/test_vot.xml
...@@ -20,7 +20,8 @@ AC_CONFIG_FILES([ \ ...@@ -20,7 +20,8 @@ AC_CONFIG_FILES([ \
python/__init__.py \ python/__init__.py \
python/bayestar/Makefile \ python/bayestar/Makefile \
swig/Makefile \ swig/Makefile \
test/Makefile test/Makefile \
test/baseline_images/Makefile
]) ])
AM_INIT_AUTOMAKE([1.11 foreign subdir-objects color-tests parallel-tests]) AM_INIT_AUTOMAKE([1.11 foreign subdir-objects color-tests parallel-tests])
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
""" """
Plotting tools for drawing skymaps Plotting tools for drawing skymaps
""" """
from __future__ import division
__author__ = "Leo Singer <leo.singer@ligo.org>" __author__ = "Leo Singer <leo.singer@ligo.org>"
__all__ = ("AstroMollweideAxes", "reference_angle", "make_rect_poly", "heatmap") __all__ = ("AstroMollweideAxes", "reference_angle", "make_rect_poly", "heatmap")
...@@ -37,6 +38,7 @@ from matplotlib.transforms import Transform, Affine2D ...@@ -37,6 +38,7 @@ from matplotlib.transforms import Transform, Affine2D
from matplotlib.projections.geo import MollweideAxes from matplotlib.projections.geo import MollweideAxes
from mpl_toolkits.basemap import _geoslib as geos from mpl_toolkits.basemap import _geoslib as geos
from matplotlib import pyplot as plt from matplotlib import pyplot as plt
import scipy.stats
import numpy as np import numpy as np
import healpy as hp import healpy as hp
...@@ -591,3 +593,205 @@ def outline_text(ax): ...@@ -591,3 +593,205 @@ def outline_text(ax):
effects = [patheffects.withStroke(linewidth=2, foreground='w')] effects = [patheffects.withStroke(linewidth=2, foreground='w')]
for artist in ax.findobj(text.Text): for artist in ax.findobj(text.Text):
artist.set_path_effects(effects) artist.set_path_effects(effects)
class PPPlot(Axes):
"""Construct a probability--probability (P--P) plot.
Example usage::
from lalinference.plot import PPPlot
from matplotlib import pyplot as plt
import numpy as np
n = 100
p_values_1 = np.random.uniform(size=n) # One experiment
p_values_2 = np.random.uniform(size=n) # Another experiment
p_values_3 = np.random.uniform(size=n) # Yet another experiment
fig = plt.figure(figsize=(3, 3))
ax = fig.add_subplot(111, projection=PPPlot)
ax.add_confidence_band(n, alpha=0.95) # Add 95% confidence band
ax.add_diagonal() # Add diagonal line
ax.add_lightning(n, 20) # Add some random realizations of n samples
ax.add_series(p_values_1, p_values_2, p_values_3) # Add our data
fig.savefig('example.png')
Or, you can also create an instance of ``PPPlot`` by calling its
constructor directly::
from lalinference.plot import PPPlot
from matplotlib import pyplot as plt
import numpy as np
rect = [0.1, 0.1, 0.8, 0.8] # Where to place axes in figure
fig = plt.figure(figsize=(3, 3))
ax = PPPlot(fig, rect)
fig.add_axes(ax)
# ...
fig.savefig('example.png')
"""
def __init__(self, *args, **kwargs):
# Call parent constructor
super(PPPlot, self).__init__(*args, **kwargs)
# Square axes, limits from 0 to 1
self.set_aspect(1.0)
self.set_xlim(0.0, 1.0)
self.set_ylim(0.0, 1.0)
@staticmethod
def _make_series(p_values):
for ps in p_values:
ps = np.sort(np.atleast_1d(ps))
n = len(ps)
xs = np.concatenate(([0.], ps, [1.]))
ys = np.concatenate(([0.], np.arange(1, n + 1) / n, [1.]))
yield xs
yield ys
def add_series(self, *p_values, **kwargs):
"""Add a series of P-values to the plot.
Parameters
----------
p_values:
One or more lists of P-values
drawstyle: ``steps`` or ``lines`` or ``default``
Plotting style. If ``steps``, then plot steps to represent a
piecewise constant function. If ``lines``, then connect points with
straight lines. If ``default`` then use steps if there are more
than 2 pixels per data point, or else lines.
Other parameters
----------------
kwargs: optional extra arguments to `~matplotlib.axes.Axes.plot`
"""
# Construct sequence of x, y pairs to pass to plot()
args = list(self._make_series(p_values))
min_n = min(len(ps) for ps in p_values)
# Make copy of kwargs to pass to plot()
kwargs = dict(kwargs)
ds = kwargs.pop('drawstyle', 'default')
if (ds == 'default' and 2 * min_n > self.bbox.width) or ds == 'lines':
kwargs['drawstyle'] = 'default'
else:
kwargs['drawstyle'] = 'steps-post'
return self.plot(*args, **kwargs)
def add_diagonal(self, *args, **kwargs):
"""Add a diagonal line to the plot, running from (0, 0) to (1, 1).
Other parameters
----------------
kwargs: optional extra arguments to `~matplotlib.axes.Axes.plot`
"""
# Make copy of kwargs to pass to plot()
kwargs = dict(kwargs)
kwargs.setdefault('color', 'black')
kwargs.setdefault('linestyle', 'dashed')
kwargs.setdefault('linewidth', 0.5)
# Plot diagonal line
return self.plot([0, 1], [0, 1], *args, **kwargs)
def add_lightning(self, nsamples, ntrials, **kwargs):
"""Add P-values drawn from a random uniform distribution, as a visual
representation of the acceptable scatter about the diagonal.
Parameters
----------
nsamples: int
Number of P-values in each trial
ntrials: int
Number of line series to draw.
Other parameters
----------------
kwargs: optional extra arguments to `~matplotlib.axes.Axes.plot`
"""
# Draw random samples
args = np.random.uniform(size=(ntrials, nsamples))
# Make copy of kwargs to pass to plot()
kwargs = dict(kwargs)
kwargs.setdefault('color', 'black')
kwargs.setdefault('alpha', 0.5)
kwargs.setdefault('linewidth', 0.25)
# Plot series
return self.add_series(*args, **kwargs)
def add_confidence_band(self, nsamples, alpha=0.95, annotate=True, **kwargs):
"""Add a target confidence band.
Parameters
----------
nsamples: int
Number of P-values
alpha: float, default: 0.95
Confidence level
annotate: bool, optional, default: True
If True, then label the confidence band.
Other parameters
----------------
kwargs: optional extra arguments to `~matplotlib.axes.Axes.fill_betweenx`
"""
n = nsamples
k = np.arange(0, n + 1)
p = k / n
ci_lo, ci_hi = scipy.stats.beta.interval(alpha, k + 1, n - k + 1)
# Make copy of kwargs to pass to fill_betweenx()
kwargs = dict(kwargs)
kwargs.setdefault('color', 'lightgray')
kwargs.setdefault('edgecolor', 'gray')
kwargs.setdefault('linewidth', 0.5)
fontsize = kwargs.pop('fontsize', 'x-small')
if annotate:
percent_sign = r'\%' if matplotlib.rcParams['text.usetex'] else '%'
label = 'target {0:g}{1:s}\nconfidence band'.format(
100 * alpha, percent_sign)
self.annotate(
label,
xy=(1, 1),
xytext=(0, 0),
xycoords='axes fraction',
textcoords='offset points',
annotation_clip=False,
horizontalalignment='right',
verticalalignment='bottom',
fontsize=fontsize,
arrowprops=dict(
arrowstyle="->",
shrinkA=0, shrinkB=2, linewidth=0.5,
connectionstyle="angle,angleA=0,angleB=45,rad=0")
)
return self.fill_betweenx(p, ci_lo, ci_hi, **kwargs)
@classmethod
def _as_mpl_axes(cls):
"""Support placement in figure using the `projection` keyword argument.
See http://matplotlib.org/devel/add_new_projection.html"""
return cls, {}
SUBDIRS = baseline_images
TEST_CPPFLAGS = -DTEST_DATA_DIR='"$(abs_srcdir)/"' TEST_CPPFLAGS = -DTEST_DATA_DIR='"$(abs_srcdir)/"'
LDADD = $(top_builddir)/src/liblalinference.la LDADD = $(top_builddir)/src/liblalinference.la
...@@ -47,7 +49,11 @@ TESTS = \ ...@@ -47,7 +49,11 @@ TESTS = \
LALInferenceKDTest \ LALInferenceKDTest \
$(XMLPRG) $(XMLPRG)
EXTRA_DIST = EXTRA_DIST = test_plot.py
# FIXME: Run this as a regular unit test.
installcheck-local:
$(PYTHON) test_plot.py
CLEANFILES = \ CLEANFILES = \
test_vot.xml \ test_vot.xml \
......
EXTRA_DIST = \
pp_plot_default.png \
pp_plot_lines.png \
pp_plot_steps.png
\ No newline at end of file
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cbook
from matplotlib.testing.compare import compare_images
import functools
import unittest
import os
import lalinference.plot
def image_comparison(testfunc, filename=None, tolerance=1):
# Construct paths to baseline and result image directories.
filedir = os.path.dirname(os.path.abspath(__file__))
baseline_dir = os.path.join(filedir, 'baseline_images')
result_dir = os.path.join(os.getcwd(), 'test_result_images')
# Default test result filename: test function name, stripped of the
# 'test_' prefix, and with '.png' appended
if filename is None:
filename = testfunc.__name__.replace('test_', '') + '.png'
# Construct full paths to baseline and test images.
baseline_path = os.path.join(baseline_dir, filename)
result_path = os.path.join(result_dir, filename)
@functools.wraps(testfunc)
def test(*args, **kwargs):
# Run test function
fig = testfunc(*args, **kwargs)
# Create directories if needed
cbook.mkdirs(baseline_dir)
cbook.mkdirs(result_dir)
if os.path.exists(baseline_path):
fig.savefig(result_path)
msg = compare_images(baseline_path, result_path, tolerance)
if msg is not None:
raise AssertionError(msg)
else:
fig.savefig(baseline_path)
raise unittest.SkipTest(
"Generated baseline image, {0}".format(baseline_path))
return test
class TestPlot(unittest.TestCase):
def setUp(self):
# Re-initialize the random seed to make the unit test repeatable
np.random.seed(0)
self.fig = plt.figure(figsize=(3, 3), dpi=72)
self.ax = self.fig.add_subplot(111, projection= lalinference.plot.PPPlot)
# self.ax = lalinference.plot.PPPlot(self.fig, [0.1, 0.1, 0.8, 0.8])
# self.fig.add_axes(self.ax)
self.p_values = np.arange(1, 20) / 20
@image_comparison
def test_pp_plot_steps(self):
"""Test P--P plot with drawstyle='steps'."""
self.ax.add_confidence_band(len(self.p_values))
self.ax.add_diagonal()
self.ax.add_lightning(len(self.p_values), 20, drawstyle='steps')
self.ax.add_series(self.p_values, drawstyle='steps')
return self.fig
@image_comparison
def test_pp_plot_lines(self):
"""Test P--P plot with drawstyle='steps'."""
self.ax.add_confidence_band(len(self.p_values))
self.ax.add_diagonal()
self.ax.add_lightning(len(self.p_values), 20, drawstyle='lines')
self.ax.add_series(self.p_values, drawstyle='lines')
self.ax.add_diagonal()
return self.fig
@image_comparison
def test_pp_plot_default(self):
"""Test P--P plot with drawstyle='steps'."""
self.ax.add_confidence_band(len(self.p_values))
self.ax.add_diagonal()
self.ax.add_lightning(len(self.p_values), 20)
self.ax.add_series(self.p_values)
return self.fig
if __name__ == '__main__':
import unittest
unittest.main()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment