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

Add PPPlot class

Original: ef4f7af18db31b9eba810f177c666aab790af2b2
parent 267092bc
......@@ -3,6 +3,7 @@
*.log
*.o
*.pc
*.pyc
.deps
.libs
/_build/
......@@ -55,4 +56,5 @@ test/LALInferencePriorTest
test/LALInferenceProposalTest
test/LALInferenceTest
test/LALInferenceXMLTest
test/test_result_images
test/test_vot.xml
......@@ -20,7 +20,8 @@ AC_CONFIG_FILES([ \
python/__init__.py \
python/bayestar/Makefile \
swig/Makefile \
test/Makefile
test/Makefile \
test/baseline_images/Makefile
])
AM_INIT_AUTOMAKE([1.11 foreign subdir-objects color-tests parallel-tests])
......
......@@ -17,6 +17,7 @@
"""
Plotting tools for drawing skymaps
"""
from __future__ import division
__author__ = "Leo Singer <leo.singer@ligo.org>"
__all__ = ("AstroMollweideAxes", "reference_angle", "make_rect_poly", "heatmap")
......@@ -37,6 +38,7 @@ from matplotlib.transforms import Transform, Affine2D
from matplotlib.projections.geo import MollweideAxes
from mpl_toolkits.basemap import _geoslib as geos
from matplotlib import pyplot as plt
import scipy.stats
import numpy as np
import healpy as hp
......@@ -591,3 +593,205 @@ def outline_text(ax):
effects = [patheffects.withStroke(linewidth=2, foreground='w')]
for artist in ax.findobj(text.Text):
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)/"'
LDADD = $(top_builddir)/src/liblalinference.la
......@@ -47,7 +49,11 @@ TESTS = \
LALInferenceKDTest \
$(XMLPRG)
EXTRA_DIST =
EXTRA_DIST = test_plot.py
# FIXME: Run this as a regular unit test.
installcheck-local:
$(PYTHON) test_plot.py
CLEANFILES = \
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