import math
[docs]
def TaylorDiagram(
stddev,
corrcoef,
refstd,
fig=None,
rect=111,
title=None,
titleprops_dict=dict(),
colors=None,
cmap=None,
normalize=False,
labels=None,
markers=None,
markersizes=None,
closed_marker=True,
markercloses=None,
zorders=None,
ref_label=None,
smax=None,
compare_models=None,
arrowprops_dict=None,
annotate_text=None,
radial_axis_title=None,
angular_axis_title="Correlation",
grid=True,
debug=False,
):
"""
Create a Taylor diagram.
.. image:: /_static/images/taylor_diagram_docstring_example.png
:alt: Example Taylor Diagram
:align: center
:width: 600px
Parameters
----------
stddev : numpy.ndarray
an array of standard deviations
corrcoef : numpy.ndarray
an array of correlation coefficients
refstd : float
the reference standard deviation
fig : matplotlib figure, optional
the matplotlib figure
rect : a 3-digit integer, optional
ax subplot rect, , default is 111, which indicate the figure has 1 row, 1 column, and this plot is the first plot.
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplot.html
https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.add_subplot
title : string, optional
title for the plot
titleprops_dict : dict, optional
title property dict (e.g., fontsize)
cmap : string, optional
a name of matplotlib colormap
https://matplotlib.org/stable/gallery/color/colormap_reference.html
colors : array, optional
an array or list of colors for each element of the input arrays
if colors is given, it will override cmap
normalize : bool, optional
disable to skip normalization of the standard deviation
default is False
labels : list, optional
list of text for labels
markers : list, optional
list of marker type
markersizes : list, optional
list of integer for marker size
closed_marker : bool, optional
closed marker or opened marker
default - True
markercloses : list of bool, optional
When closed_marker is False but you still want to have a few closed markers among opened markers, provide list of True (close) or False (open)
default - None
zorders : list, optional
list of integer for zorder
ref_label : str, optional
label for reference data
smax : int or float, optional
maximum of axis range for (normalized) standard deviation
compare_models : list of tuples, optional
list of pair of two models to compare by showing arrows
arrowprops_dict: dict, optional
dict for matplotlib annotation arrowprops for compare_models arrow
See https://matplotlib.org/stable/tutorials/text/annotations.html for details
annotate_text : string, optional
text to place at the begining of the comparing arrow
radial_axis_title : string, optional
axis title for radial axis
default - Standard deviation (when normalize=False) or Normalized standard deviation (when normalize=True)
angular_axis_title : string, optional
axis title for angular axis
default - Correlation
grid : bool, optional
grid line in plot
default - True
debug : bool, optional
default - False
if true print some interim results for debugging purpose
Returns
-------
fig : matplotlib figure
the matplotlib figure
ax : matplotlib axis
the matplotlib axis
Notes
-----
This code was adpated from the ILAMB code that was written by Nathan Collier (ORNL)
(https://github.com/rubisco-sfa/ILAMB/blob/6780ef0824a8a245ae60e518d5b5fc25f970f3d6/src/ILAMB/Post.py#L101)
and revised by Jiwoo Lee (LLNL) to add capabilities and enable additional customizations
for implementation into PCMDI Metrics Package (PMP).
The original code was written by Yannick Copin (https://gist.github.com/ycopin/3342888).
Reference for Taylor Diagram: Taylor, K. E. (2001), Summarizing multiple aspects of model performance in a single diagram,
J. Geophys. Res., 106(D7), 7183–7192, http://dx.doi.org/10.1029/2000JD900719
Author: Jiwoo Lee (PCMDI LLNL)
Update history:
- 2022-03 First implemented
- 2024-11 Docstring cleaned up
Examples
--------
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from pcmdi_metrics.graphics import TaylorDiagram
>>>
>>> stddev = np.random.uniform(low=1, high=10, size=(10,)) # Generate 10 random numbers between 1 and 10
>>> corrcoeff = np.random.uniform(low=0.5, high=1, size=(10,)) # Generate 10 random numbers between 0.5 and 1
>>> refstd = 5
>>> models = ['model '+str(i) for i in range(1,11)]
>>>
>>> fig = plt.figure(figsize=(5,5))
>>>
>>> fig, ax = TaylorDiagram(stddev, corrcoeff, refstd,
fig=fig,
labels=models,
ref_label='Reference'
)
>>>
>>> ax.legend(bbox_to_anchor=(1.05, 0), loc='lower left', ncol=2)
>>> fig.suptitle('Example Taylor Diagram', fontsize=20)
.. image:: /_static/images/taylor_diagram_docstring_example.png
:alt: Example Taylor Diagram
:align: center
:width: 600px
Further examples can be found `here <https://github.com/PCMDI/pcmdi_metrics/tree/main/pcmdi_metrics/graphics/taylor_diagram#readme>`__.
"""
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist.floating_axes as FA
import mpl_toolkits.axisartist.grid_finder as GF
import numpy as np
from matplotlib.projections import PolarAxes
# define transform
tr = PolarAxes.PolarTransform()
# correlation labels
rlocs = np.concatenate((np.arange(10) / 10.0, [0.95, 0.99]))
tlocs = np.arccos(rlocs)
gl1 = GF.FixedLocator(tlocs)
tf1 = GF.DictFormatter(dict(zip(tlocs, map(str, rlocs))))
# standard deviation axis extent
if normalize:
stddev = stddev / refstd
refstd = 1.0
# Radial axis range
smin = 0
if smax is None:
smax = max(2.0, 1.1 * stddev.max())
# add the curvilinear grid
ghelper = FA.GridHelperCurveLinear(
tr, extremes=(0, np.pi / 2, smin, smax), grid_locator1=gl1, tick_formatter1=tf1
)
if fig is None:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(rect, axes_class=FA.FloatingAxes, grid_helper=ghelper)
if title is not None:
ax.set_title(title, **titleprops_dict)
if colors is None:
if cmap is None:
cmap = "viridis"
cm = plt.get_cmap(cmap)
colors = cm(np.linspace(0.1, 0.9, len(stddev)))
if radial_axis_title is None:
if normalize:
radial_axis_title = "Normalized standard deviation"
else:
radial_axis_title = "Standard deviation"
# adjust axes
ax.axis["top"].set_axis_direction("bottom")
ax.axis["top"].toggle(ticklabels=True, label=True)
ax.axis["top"].major_ticklabels.set_axis_direction("top")
ax.axis["top"].label.set_axis_direction("top")
ax.axis["top"].label.set_text(angular_axis_title)
ax.axis["left"].set_axis_direction("bottom")
ax.axis["left"].label.set_text(radial_axis_title)
ax.axis["right"].set_axis_direction("top")
ax.axis["right"].toggle(ticklabels=True)
ax.axis["right"].major_ticklabels.set_axis_direction("left")
ax.axis["bottom"].set_visible(False)
ax.grid(grid)
ax = ax.get_aux_axes(tr)
# Add reference point and stddev contour
ax.plot([0], refstd, "k*", ms=12, mew=0, label=ref_label)
t = np.linspace(0, np.pi / 2)
r = np.zeros_like(t) + refstd
ax.plot(t, r, "k--")
# centralized rms contours
rs, ts = np.meshgrid(np.linspace(smin, smax), np.linspace(0, np.pi / 2))
rms = np.sqrt(refstd**2 + rs**2 - 2 * refstd * rs * np.cos(ts))
contours = ax.contour(ts, rs, rms, 5, colors="k", alpha=0.4)
ax.clabel(contours, fmt="%1.1f")
# Plot data
corrcoef = corrcoef.clip(-1, 1)
for i in range(len(corrcoef)):
# --- customize start ---
# customize label
if labels is None:
label = None
else:
label = labels[i]
# customize marker
if markers is None:
marker = "o"
else:
marker = markers[i]
# customize marker size
if markersizes is None:
ms = 8
else:
ms = markersizes[i]
# customize marker order
if zorders is None:
zorder = None
else:
zorder = zorders[i]
# customize marker closed/opened
if closed_marker:
markerclose = True
else:
if markercloses is None:
markerclose = False
else:
markerclose = markercloses[i]
if markerclose:
if closed_marker:
marker_dict = dict(
color=colors[i],
mew=0,
)
else:
marker_dict = dict(mfc=colors[i], mec="k", mew=1)
else:
marker_dict = dict(
mec=colors[i],
mfc="none",
mew=1,
)
# --- customize end ---
# -------------------------
# place marker on the graph
# -------------------------
ax.plot(
np.arccos(corrcoef[i]),
stddev[i],
marker,
ms=ms,
label=label,
zorder=zorder,
**marker_dict,
)
# debugging
if debug:
crmsd = math.sqrt(
stddev[i] ** 2 + refstd**2 - 2 * stddev[i] * refstd * corrcoef[i]
) # centered rms difference
print(
"i, label, corrcoef[i], np.arccos(corrcoef[i]), stddev[i], crmsd:",
i,
label,
corrcoef[i],
np.arccos(corrcoef[i]),
stddev[i],
crmsd,
)
# Add arrow(s)
if arrowprops_dict is None:
arrowprops_dict = dict(
facecolor="black", lw=0.5, width=0.5, shrink=0.05
) # shrink arrow length little bit to make it look good...
if compare_models is not None:
for compare_models_pair in compare_models:
index_model1 = labels.index(compare_models_pair[0])
index_model2 = labels.index(compare_models_pair[1])
theta1 = np.arccos(corrcoef[index_model1])
theta2 = np.arccos(corrcoef[index_model2])
r1 = stddev[index_model1]
r2 = stddev[index_model2]
ax.annotate(
annotate_text,
xy=(theta2, r2), # theta, radius of arrival
xytext=(theta1, r1), # theta, radius of departure
xycoords="data",
textcoords="data",
arrowprops=arrowprops_dict,
horizontalalignment="center",
verticalalignment="center",
)
return fig, ax