Skip to content

Instantly share code, notes, and snippets.

@ycopin
Last active August 9, 2024 14:25
Show Gist options
  • Save ycopin/3342888 to your computer and use it in GitHub Desktop.
Save ycopin/3342888 to your computer and use it in GitHub Desktop.
Taylor diagram for python/matplotlib [ 10.5281/zenodo.5548061 ]
#!/usr/bin/env python
# Copyright: This document has been placed in the public domain.
"""
Taylor diagram (Taylor, 2001) implementation.
Note: If you have found these software useful for your research, I would
appreciate an acknowledgment.
"""
__version__ = "Time-stamp: <2018-12-06 11:43:41 ycopin>"
__author__ = "Yannick Copin <[email protected]>"
import numpy as NP
import matplotlib.pyplot as PLT
class TaylorDiagram(object):
"""
Taylor diagram.
Plot model standard deviation and correlation to reference (data)
sample in a single-quadrant polar plot, with r=stddev and
theta=arccos(correlation).
"""
def __init__(self, refstd,
fig=None, rect=111, label='_', srange=(0, 1.5), extend=False):
"""
Set up Taylor diagram axes, i.e. single quadrant polar
plot, using `mpl_toolkits.axisartist.floating_axes`.
Parameters:
* refstd: reference standard deviation to be compared to
* fig: input Figure or None
* rect: subplot definition
* label: reference label
* srange: stddev axis extension, in units of *refstd*
* extend: extend diagram to negative correlations
"""
from matplotlib.projections import PolarAxes
import mpl_toolkits.axisartist.floating_axes as FA
import mpl_toolkits.axisartist.grid_finder as GF
self.refstd = refstd # Reference standard deviation
tr = PolarAxes.PolarTransform()
# Correlation labels
rlocs = NP.array([0, 0.2, 0.4, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1])
if extend:
# Diagram extended to negative correlations
self.tmax = NP.pi
rlocs = NP.concatenate((-rlocs[:0:-1], rlocs))
else:
# Diagram limited to positive correlations
self.tmax = NP.pi/2
tlocs = NP.arccos(rlocs) # Conversion to polar angles
gl1 = GF.FixedLocator(tlocs) # Positions
tf1 = GF.DictFormatter(dict(zip(tlocs, map(str, rlocs))))
# Standard deviation axis extent (in units of reference stddev)
self.smin = srange[0] * self.refstd
self.smax = srange[1] * self.refstd
ghelper = FA.GridHelperCurveLinear(
tr,
extremes=(0, self.tmax, self.smin, self.smax),
grid_locator1=gl1, tick_formatter1=tf1)
if fig is None:
fig = PLT.figure()
ax = FA.FloatingSubplot(fig, rect, grid_helper=ghelper)
fig.add_subplot(ax)
# Adjust axes
ax.axis["top"].set_axis_direction("bottom") # "Angle axis"
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("Correlation")
ax.axis["left"].set_axis_direction("bottom") # "X axis"
ax.axis["left"].label.set_text("Standard deviation")
ax.axis["right"].set_axis_direction("top") # "Y-axis"
ax.axis["right"].toggle(ticklabels=True)
ax.axis["right"].major_ticklabels.set_axis_direction(
"bottom" if extend else "left")
if self.smin:
ax.axis["bottom"].toggle(ticklabels=False, label=False)
else:
ax.axis["bottom"].set_visible(False) # Unused
self._ax = ax # Graphical axes
self.ax = ax.get_aux_axes(tr) # Polar coordinates
# Add reference point and stddev contour
l, = self.ax.plot([0], self.refstd, 'k*',
ls='', ms=10, label=label)
t = NP.linspace(0, self.tmax)
r = NP.zeros_like(t) + self.refstd
self.ax.plot(t, r, 'k--', label='_')
# Collect sample points for latter use (e.g. legend)
self.samplePoints = [l]
def add_sample(self, stddev, corrcoef, *args, **kwargs):
"""
Add sample (*stddev*, *corrcoeff*) to the Taylor
diagram. *args* and *kwargs* are directly propagated to the
`Figure.plot` command.
"""
l, = self.ax.plot(NP.arccos(corrcoef), stddev,
*args, **kwargs) # (theta, radius)
self.samplePoints.append(l)
return l
def add_grid(self, *args, **kwargs):
"""Add a grid."""
self._ax.grid(*args, **kwargs)
def add_contours(self, levels=5, **kwargs):
"""
Add constant centered RMS difference contours, defined by *levels*.
"""
rs, ts = NP.meshgrid(NP.linspace(self.smin, self.smax),
NP.linspace(0, self.tmax))
# Compute centered RMS difference
rms = NP.sqrt(self.refstd**2 + rs**2 - 2*self.refstd*rs*NP.cos(ts))
contours = self.ax.contour(ts, rs, rms, levels, **kwargs)
return contours
def test1():
"""Display a Taylor diagram in a separate axis."""
# Reference dataset
x = NP.linspace(0, 4*NP.pi, 100)
data = NP.sin(x)
refstd = data.std(ddof=1) # Reference standard deviation
# Generate models
m1 = data + 0.2*NP.random.randn(len(x)) # Model 1
m2 = 0.8*data + .1*NP.random.randn(len(x)) # Model 2
m3 = NP.sin(x-NP.pi/10) # Model 3
# Compute stddev and correlation coefficient of models
samples = NP.array([ [m.std(ddof=1), NP.corrcoef(data, m)[0, 1]]
for m in (m1, m2, m3)])
fig = PLT.figure(figsize=(10, 4))
ax1 = fig.add_subplot(1, 2, 1, xlabel='X', ylabel='Y')
# Taylor diagram
dia = TaylorDiagram(refstd, fig=fig, rect=122, label="Reference",
srange=(0.5, 1.5))
colors = PLT.matplotlib.cm.jet(NP.linspace(0, 1, len(samples)))
ax1.plot(x, data, 'ko', label='Data')
for i, m in enumerate([m1, m2, m3]):
ax1.plot(x, m, c=colors[i], label='Model %d' % (i+1))
ax1.legend(numpoints=1, prop=dict(size='small'), loc='best')
# Add the models to Taylor diagram
for i, (stddev, corrcoef) in enumerate(samples):
dia.add_sample(stddev, corrcoef,
marker='$%d$' % (i+1), ms=10, ls='',
mfc=colors[i], mec=colors[i],
label="Model %d" % (i+1))
# Add grid
dia.add_grid()
# Add RMS contours, and label them
contours = dia.add_contours(colors='0.5')
PLT.clabel(contours, inline=1, fontsize=10, fmt='%.2f')
# Add a figure legend
fig.legend(dia.samplePoints,
[ p.get_label() for p in dia.samplePoints ],
numpoints=1, prop=dict(size='small'), loc='upper right')
return dia
def test2():
"""
Climatology-oriented example (after iteration w/ Michael A. Rawlins).
"""
# Reference std
stdref = 48.491
# Samples std,rho,name
samples = [[25.939, 0.385, "Model A"],
[29.593, 0.509, "Model B"],
[33.125, 0.585, "Model C"],
[29.593, 0.509, "Model D"],
[71.215, 0.473, "Model E"],
[27.062, 0.360, "Model F"],
[38.449, 0.342, "Model G"],
[35.807, 0.609, "Model H"],
[17.831, 0.360, "Model I"]]
fig = PLT.figure()
dia = TaylorDiagram(stdref, fig=fig, label='Reference', extend=True)
dia.samplePoints[0].set_color('r') # Mark reference point as a red star
# Add models to Taylor diagram
for i, (stddev, corrcoef, name) in enumerate(samples):
dia.add_sample(stddev, corrcoef,
marker='$%d$' % (i+1), ms=10, ls='',
mfc='k', mec='k',
label=name)
# Add RMS contours, and label them
contours = dia.add_contours(levels=5, colors='0.5') # 5 levels in grey
PLT.clabel(contours, inline=1, fontsize=10, fmt='%.0f')
dia.add_grid() # Add grid
dia._ax.axis[:].major_ticks.set_tick_out(True) # Put ticks outward
# Add a figure legend and title
fig.legend(dia.samplePoints,
[ p.get_label() for p in dia.samplePoints ],
numpoints=1, prop=dict(size='small'), loc='upper right')
fig.suptitle("Taylor diagram", size='x-large') # Figure title
return dia
if __name__ == '__main__':
dia = test1()
dia = test2()
PLT.show()
#!/usr/bin/env python
__version__ = "Time-stamp: <2018-12-06 11:55:22 ycopin>"
__author__ = "Yannick Copin <[email protected]>"
"""
Example of use of TaylorDiagram. Illustration dataset courtesy of Michael
Rawlins.
Rawlins, M. A., R. S. Bradley, H. F. Diaz, 2012. Assessment of regional climate
model simulation estimates over the Northeast United States, Journal of
Geophysical Research (2012JGRD..11723112R).
"""
from taylorDiagram import TaylorDiagram
import numpy as NP
import matplotlib.pyplot as PLT
# Reference std
stdrefs = dict(winter=48.491,
spring=44.927,
summer=37.664,
autumn=41.589)
# Sample std,rho: Be sure to check order and that correct numbers are placed!
samples = dict(winter=[[17.831, 0.360, "CCSM CRCM"],
[27.062, 0.360, "CCSM MM5"],
[33.125, 0.585, "CCSM WRFG"],
[25.939, 0.385, "CGCM3 CRCM"],
[29.593, 0.509, "CGCM3 RCM3"],
[35.807, 0.609, "CGCM3 WRFG"],
[38.449, 0.342, "GFDL ECP2"],
[29.593, 0.509, "GFDL RCM3"],
[71.215, 0.473, "HADCM3 HRM3"]],
spring=[[32.174, -0.262, "CCSM CRCM"],
[24.042, -0.055, "CCSM MM5"],
[29.647, -0.040, "CCSM WRFG"],
[22.820, 0.222, "CGCM3 CRCM"],
[20.505, 0.445, "CGCM3 RCM3"],
[26.917, 0.332, "CGCM3 WRFG"],
[25.776, 0.366, "GFDL ECP2"],
[18.018, 0.452, "GFDL RCM3"],
[79.875, 0.447, "HADCM3 HRM3"]],
summer=[[35.863, 0.096, "CCSM CRCM"],
[43.771, 0.367, "CCSM MM5"],
[35.890, 0.267, "CCSM WRFG"],
[49.658, 0.134, "CGCM3 CRCM"],
[28.972, 0.027, "CGCM3 RCM3"],
[60.396, 0.191, "CGCM3 WRFG"],
[46.529, 0.258, "GFDL ECP2"],
[35.230, -0.014, "GFDL RCM3"],
[87.562, 0.503, "HADCM3 HRM3"]],
autumn=[[27.374, 0.150, "CCSM CRCM"],
[20.270, 0.451, "CCSM MM5"],
[21.070, 0.505, "CCSM WRFG"],
[25.666, 0.517, "CGCM3 CRCM"],
[35.073, 0.205, "CGCM3 RCM3"],
[25.666, 0.517, "CGCM3 WRFG"],
[23.409, 0.353, "GFDL ECP2"],
[29.367, 0.235, "GFDL RCM3"],
[70.065, 0.444, "HADCM3 HRM3"]])
# Colormap (see http://www.scipy.org/Cookbook/Matplotlib/Show_colormaps)
colors = PLT.matplotlib.cm.Set1(NP.linspace(0,1,len(samples['winter'])))
# Here set placement of the points marking 95th and 99th significance
# levels. For more than 102 samples (degrees freedom > 100), critical
# correlation levels are 0.195 and 0.254 for 95th and 99th
# significance levels respectively. Set these by eyeball using the
# standard deviation x and y axis.
#x95 = [0.01, 0.68] # For Tair, this is for 95th level (r = 0.195)
#y95 = [0.0, 3.45]
#x99 = [0.01, 0.95] # For Tair, this is for 99th level (r = 0.254)
#y99 = [0.0, 3.45]
x95 = [0.05, 13.9] # For Prcp, this is for 95th level (r = 0.195)
y95 = [0.0, 71.0]
x99 = [0.05, 19.0] # For Prcp, this is for 99th level (r = 0.254)
y99 = [0.0, 70.0]
rects = dict(winter=221,
spring=222,
summer=223,
autumn=224)
fig = PLT.figure(figsize=(11,8))
fig.suptitle("Precipitations", size='x-large')
for season in ['winter','spring','summer','autumn']:
dia = TaylorDiagram(stdrefs[season], fig=fig, rect=rects[season],
label='Reference')
dia.ax.plot(x95,y95,color='k')
dia.ax.plot(x99,y99,color='k')
# Add samples to Taylor diagram
for i,(stddev,corrcoef,name) in enumerate(samples[season]):
dia.add_sample(stddev, corrcoef,
marker='$%d$' % (i+1), ms=10, ls='',
#mfc='k', mec='k', # B&W
mfc=colors[i], mec=colors[i], # Colors
label=name)
# Add RMS contours, and label them
contours = dia.add_contours(levels=5, colors='0.5') # 5 levels
dia.ax.clabel(contours, inline=1, fontsize=10, fmt='%.1f')
# Tricky: ax is the polar ax (used for plots), _ax is the
# container (used for layout)
dia._ax.set_title(season.capitalize())
# Add a figure legend and title. For loc option, place x,y tuple inside [ ].
# Can also use special options here:
# http://matplotlib.sourceforge.net/users/legend_guide.html
fig.legend(dia.samplePoints,
[ p.get_label() for p in dia.samplePoints ],
numpoints=1, prop=dict(size='small'), loc='center')
fig.tight_layout()
PLT.savefig('test_taylor_4panel.png')
PLT.show()
@ycopin
Copy link
Author

ycopin commented Jun 22, 2022

Hi @ycopin, thanks for sharing this code!! I made a plot and my points are accumulated in a small area, most of them overllaping, not a graph very easy to read. I was wondering how to re-scale the correlation coefficient contours. For example in the very top of y-axis the correlation coefficient is 0, I would like it to start from 0.8, so the limits of Cor. coeff. would be from 0.8 to 1.0 and thus my points would spread across the whole graph.

That would not follow anymore the Taylor diagram theory, see https://en.wikipedia.org/wiki/Taylor_diagram#Theoretical_basis

@ashishShaji333
Copy link

Hi @ycopin,
Could do help me to modify the axis of the plot?. LIke the size of the ticks, ticklabels etc., the rotation of the ticklabels

@ycopin
Copy link
Author

ycopin commented Aug 23, 2022

Hi @ycopin, Could do help me to modify the axis of the plot?. LIke the size of the ticks, ticklabels etc., the rotation of the ticklabels

The editable axes is self._ax (see L99 and e.g. L80-92).

@Elcook
Copy link

Elcook commented Sep 1, 2022

Thanks for the code @ycopin.

How would you go about changing the color of the X or Y grid lines only?

I've tried
dia.add_grid()
a=dia._ax.get_ygridlines()
a[4].set_color('red')

But to no avail.

Could you point me to the right direction please?

@jess253
Copy link

jess253 commented Oct 11, 2022 via email

@ycopin
Copy link
Author

ycopin commented Oct 11, 2022

@jess253 What do you mean? You're basically inputing everything to the Taylor plot, and nothing is really computed in there...

@lee1043
Copy link

lee1043 commented Oct 11, 2022

@jess253, @ycopin's code takes standard deviations of model and reference, and correlation coefficient for input, and plotting them on a polar plot by converting them to radius (standard deviation) and angle (correlation). Centered RMSD can be derived from standard deviations of model and reference, and correlation coefficient. I think you can do something like below, which I followed the Taylor 2001 paper.

crmsd = math.sqrt(stddev**2 + refstd**2 - 2 * stddev * refstd * corrcoef)  # centered rms difference

@Bluedot96
Copy link

Bluedot96 commented Nov 15, 2022

Hi @ycopin,

Thanks for sharing your code.
How can I make the size of the markers uniform? Which part of your code controls it? Can you please help?

@ycopin
Copy link
Author

ycopin commented Nov 15, 2022

The size of the markers is supposed to be uniform (I use plot, not scatter), so I'm not sure what is your problem...

@lee1043
Copy link

lee1043 commented Nov 15, 2022

@Bluedot96 I believe ms parameter in the dia.add_sample controls the marker size, in case you want to change.

@AbdulHadi01
Copy link

I am new to python, I have six model outputs all in netCDF format which I what to use in the Taylor diagram. At what stage would I declare or import the variable in this code?

@krikru
Copy link

krikru commented Oct 3, 2023

When I try this code, the Taylor diagram looks fine when I do plt.show(), but when I save it as a PNG or PDF file, it gets slightly squished, in the sense that the vertical standard deviation axis becomes shorter (by maybe 20%) than the horizontal standard deviation axis. Has anyone else experienced this problem? Anyone knows what might cause it and what the solution is?

@ycopin
Copy link
Author

ycopin commented Oct 3, 2023

when I save it as a PNG or PDF file, it gets slightly squished, in the sense that the vertical standard deviation axis becomes shorter (by maybe 20%) than the horizontal standard deviation axis.

I don't know, maybe you can force the axes aspect to equal?

@krikru
Copy link

krikru commented Oct 4, 2023

Usingax.axis("equal") seems to fix the aspect ratio issue. However, while saving the figure generated by test2 as the code is now (or doing ax.set_aspect('equal', adjustable='box') and then saving) yields this image (where the aspect ratio is clearly off) and adding either ax.axis("equal") or ax.set_aspect('equal', adjustable='datalim') yields this image, which seems to have correct aspect ratio, the sides are now have now been cut off. The corresponding thing, but less extreme, happens to the Taylor diagram when I try to save the figure generated by test1.

I'm not knowledgeable enough about Matplotlib to know how to prevent the sides from being cut off. Do you have any idea of how to solve that?

@ycopin
Copy link
Author

ycopin commented Oct 4, 2023

Sorry I cannot really help. Did you try to set aspect of _ax rather than ax (there are 2 axes system, but I don't remember the difference... Maybe check the code). Which matplotlib version are you using? It might also be worth/needed to update this old code to latest mpl versions...

@krikru
Copy link

krikru commented Oct 4, 2023

With some more experimentation, I was able to prevent the edges from being cut off by using the code

ax.set_xlim([self.smax * -((1 if extend else 0) + margin), self.smax * (1 + margin)])
ax.set_ylim([self.smax * -margin, self.smax * (1 + margin)])

where I have margin set to 0.25 (anything smaller seems to cause a cut-off).

An interesting thing is that the numbers that annotate the axes don't seem to be cut off by the same mechanism, but if the part of the axis that they are supposed to annotate is cut off, the numbers don't show up.

I'm also experimenting a bit with using plt.tight_layout() and with using the bbox_inches='tight' option in plt.savefig (they seem to have different effects), and I think the combination with both activated seems to produce the best results (so far).

@ycopin
Copy link
Author

ycopin commented Oct 4, 2023

Feel free to provide a Change Request! :-)

@krikru
Copy link

krikru commented Oct 5, 2023

Sorry I cannot really help. Did you try to set aspect of _ax rather than ax (there are 2 axes system, but I don't remember the difference... Maybe check the code).

I only tried it on

self._ax = ax                   # Graphical axes

and not on

self.ax = ax.get_aux_axes(tr)   # Polar coordinates

Which matplotlib version are you using? It might also be worth/needed to update this old code to latest mpl versions...

pip list yields 3.6.1 for matplotlib so I guess that's the version of Matplotlib I use.

@krikru
Copy link

krikru commented Oct 20, 2023

I have noticed that when I plot curves using self._ax instead of self.ax, I get different a different line width, making the figure seem inconsistent. Do you have any idea why using self._ax instead results in a different line width?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment