# License: Apache-2.0
# Author: LKouadio <etanoyau@gmail.com>
from __future__ import annotations
import warnings
from numbers import Integral
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from ..compat.sklearn import StrOptions, validate_params
from ..utils.handlers import columns_manager
from ..utils.mathext import minmax_scaler
from ..utils.validator import (
check_consistent_length,
contains_nested_objects,
validate_length_range,
validate_yy,
)
from ._properties import TDG_DIRECTIONS
__all__ = [
"plot_taylor_diagram",
"plot_taylor_diagram_in",
"taylor_diagram",
]
[docs]
def taylor_diagram(
stddev=None,
corrcoef=None,
y_preds=None,
reference=None,
names=None,
ref_std=1,
cmap=None,
draw_ref_arc=False,
radial_strategy="rwf",
norm_c=False,
power_scaling=1.0,
marker="o",
ref_props=None,
fig_size=None,
size_props=None,
title=None,
savefig=None,
ax=None,
):
r"""
Plot a Taylor diagram to compare multiple predictions against
a reference by visualizing their correlation and standard
deviation. This function can accept either precomputed
statistics (i.e. `stddev` and `corrcoef`) or the actual arrays
(`y_preds` and `reference`) from which these statistics will be
derived.
The radial axis represents the standard deviation (std. dev.),
while the angular axis represents the correlation with the
reference (with angle :math:`\theta = \arccos(\rho)`).
Parameters
----------
stddev : list of float or None, optional
List of standard deviations for each prediction. If
`None`, the standard deviations are computed internally
from `y_preds`. The length of `stddev` should match the
number of models if provided.
corrcoef : list of float or None, optional
List of correlation coefficients for each prediction
against the reference. If `None`, these are computed
internally from `y_preds`. Must match the length of
`stddev` if provided.
y_preds : list of array-like or None, optional
One or more prediction arrays (e.g. model outputs).
Each array must share the same length as `reference`.
Required if `stddev` or `corrcoef` is not provided.
reference : array-like or None, optional
Reference (observed) array used for computing correlation
and std. dev. of predictions if `stddev` or `corrcoef`
is not given. Must share length with each prediction in
`y_preds`.
names : list of str or None, optional
Labels for each prediction array. Must match the number
of models in `y_preds` or in `stddev`/`corrcoef`. If
`None`, default labels of the form "Model_i" are used.
ref_std : float, optional
Standard deviation of the reference if already known or
desired to be set explicitly. If predictions are provided
(`y_preds` and `reference`), this is computed as
`np.std(reference)` by default.
cmap : str or None, optional
Matplotlib colormap for the background shading. If not
`None`, a contour fill is created based on the chosen
`radial_strategy`, visualizing different performance
or weighting zones. For example, `'viridis'` or
`'plasma'`.
draw_ref_arc : bool, optional
If `True`, an arc is drawn at the reference's standard
deviation, highlighting that radial distance. If `False`,
a point is placed at angle `0` with radial distance
`ref_std`. Default is `False`.
radial_strategy : {'rwf', 'convergence', 'center_focus',
'performance'}, optional
Strategy for computing the background mesh (when
`cmap` is not `None`):
* ``'rwf'``: Radial weighting function that uses
correlation and deviation distance in an exponential
form.
* ``'convergence'``: A simple radial function of `r`.
* ``'center_focus'``: Focus on a center region in the
(theta, r) space using an exponential decay from the
center.
* ``'performance'``: Highlight the region near the best
performing model (max correlation, optimal std. dev.).
norm_c : bool, optional
If `True`, the generated background mesh is normalized to
the range [0, 1] before plotting. This can highlight
relative differences more clearly. Default is `False`.
power_scaling : float, optional
When `norm_c` is `True`, the normalized background mesh
can be exponentiated by this factor. Useful for adjusting
contrast. Default is `1.0`.
marker : str, optional
Marker style for the points representing each prediction.
Defaults to `'o'`.
ref_props : dict or None, optional
Dictionary of reference plot properties, such as line
style, color, or width. Supported keys include:
* ``'label'``: Legend label for the reference.
* ``'lc'``: Line color/style for the reference arc.
* ``'color'``: Color/style for the reference point.
* ``'lw'``: Line width.
If not given, defaults to a green line and black point.
fig_size : (float, float) or None, optional
Figure size in inches, e.g. ``(width, height)``.
Defaults to ``(8, 6)``.
size_props : dict or None, optional
Optional dictionary to control tick and label sizes.
For instance:
``{'ticks': 12, 'labels': 14}``.
Can be used to adjust the font sizes of the radial and
angular ticks and labels.
title : str or None, optional
Title of the figure. If `None`, defaults to
``"Taylor Diagram"``.
savefig : str or None, optional
Path to save the figure (e.g. ``"diagram.png"``). If
`None`, the figure is displayed instead of being saved.
Notes
-----
The Taylor diagram simultaneously shows two statistics for
each model prediction :math:`p` compared to a reference
:math:`r`:
1. **Standard Deviation**:
.. math::
\sigma_p = \sqrt{\frac{1}{n}
\sum_{i=1}^{n}\bigl(p_i - \bar{p}\bigr)^2}
where :math:`\bar{p}` is the mean of :math:`p`.
2. **Correlation**: :math:`\rho`
.. math::
\rho = \frac{\mathrm{Cov}(p, r)}
{\sigma_p \; \sigma_r}
where :math:`\mathrm{Cov}(p, r)` is the covariance between
:math:`p` and :math:`r`, and :math:`\sigma_r` is the
standard deviation of :math:`r`.
The diagram uses polar coordinates with radius corresponding
to the standard deviation, and the angle
:math:`\theta = \arccos(\rho)` representing correlation.
Examples
--------
>>> import numpy as np
>>> from kdiagram.plot.evaluation import taylor_diagram
>>> # Generate synthetic data
>>> ref = np.random.randn(100)
>>> preds = [
... ref + 0.1 * np.random.randn(100),
... 1.2 * ref + 0.5 * np.random.randn(100),
... ]
>>> # Basic usage (auto-compute stddev and corrcoef)
>>> taylor_diagram(y_preds=preds, reference=ref)
See Also
--------
numpy.std : Compute standard deviation.
numpy.corrcoef : Compute correlation coefficients.
References
----------
.. [1] Taylor, K. E. (2001). Summarizing multiple aspects of
model performance in a single diagram. *Journal of
Geophysical Research*, 106(D7), 7183-7192.
"""
# Create polar subplot
if ax is None:
fig, ax = plt.subplots(
subplot_kw={"projection": "polar"},
figsize=fig_size or (8, 6),
)
else:
fig = ax.figure
# Handle reference properties
ref_props = ref_props or {}
ref_label = ref_props.pop("label", "Reference")
ref_color = ref_props.pop("lc", "red")
ref_point = ref_props.pop("color", "k*")
ref_lw = ref_props.pop("lw", 2)
# Compute stddev and corrcoef from predictions if needed
if stddev is None or corrcoef is None:
if y_preds is None or reference is None:
raise ValueError(
"Provide either stddev and corrcoef, "
"or y_preds and reference."
)
if not contains_nested_objects(y_preds, strict=True):
y_preds = [y_preds]
y_preds = [
validate_yy(reference, pred, flatten="auto")[1]
for pred in y_preds
]
stddev = [np.std(pred) for pred in y_preds]
corrcoef = [np.corrcoef(pred, reference)[0, 1] for pred in y_preds]
ref_std = np.std(reference)
# Re-check consistency
check_consistent_length(stddev, corrcoef)
# Ensure `names` matches number of models
if names is not None:
names = columns_manager(names)
if len(names) < len(stddev):
additional = [
f"Model_{i + 1}" for i in range(len(stddev) - len(names))
]
names = names + additional
else:
names = [f"Model_{i + 1}" for i in range(len(stddev))]
# Generate background if cmap is provided
if cmap:
theta_bg, r_bg = np.meshgrid(
np.linspace(0, np.pi / 2, 500),
np.linspace(0, max(stddev) + 0.5, 500),
)
# Compute background based on strategy
if radial_strategy == "convergence":
background = r_bg
elif radial_strategy == "rwf":
corr_bg = np.cos(theta_bg)
std_diff = (r_bg - ref_std) ** 2
background = np.exp(-std_diff / 0.1) * corr_bg**2
elif radial_strategy == "center_focus":
center_std = (max(stddev) + ref_std) / 2
std_diff = (r_bg - center_std) ** 2
theta_diff = (theta_bg - np.pi / 4) ** 2
background = np.exp(-std_diff / 0.1) * np.exp(-theta_diff / 0.2)
elif radial_strategy == "performance":
best_idx = np.argmax(corrcoef)
std_best = stddev[best_idx]
corr_best = corrcoef[best_idx]
theta_best = np.arccos(corr_best)
std_diff = (r_bg - std_best) ** 2
theta_diff = (theta_bg - theta_best) ** 2
background = np.exp(-std_diff / 0.05) * np.exp(-theta_diff / 0.05)
# Normalize background if requested
if norm_c:
background = minmax_scaler(background)
# background = (
# (background - np.min(background)) /
# (np.max(background) - np.min(background))
# )
background = background**power_scaling
# Plot the colored contour
ax.contourf(
theta_bg, r_bg, background, levels=100, cmap=cmap, alpha=0.8
)
# Draw reference point or arc
if draw_ref_arc:
t_arc = np.linspace(0, np.pi / 2, 500)
ax.plot(
t_arc,
[ref_std] * len(t_arc),
ref_color,
linewidth=ref_lw,
label=ref_label,
)
else:
ax.plot(0, ref_std, ref_point, markersize=12, label=ref_label)
# Plot data points
for i, (std_val, corr_val) in enumerate(zip(stddev, corrcoef)):
theta_pt = np.arccos(corr_val)
ax.plot(theta_pt, std_val, marker, label=names[i], markersize=10)
# Add correlation lines (dotted radial lines)
t_corr = np.linspace(0, np.pi / 2, 100)
for r_line in np.linspace(0, 1, 11):
ax.plot(t_corr, [r_line * ref_std] * len(t_corr), "k--", alpha=0.3)
# Add standard deviation circles
for r_circ in np.linspace(0, max(stddev) + 0.5, 5):
ax.plot(
np.linspace(0, np.pi / 2, 100), [r_circ] * 100, "k--", alpha=0.3
)
# Set axis limits
ax.set_xlim(0, np.pi / 2)
ax.set_ylim(0, max(stddev) + 0.5)
# Set x-ticks for correlation
ax.set_xticks(np.arccos(np.linspace(0, 1, 6)))
ax.set_xticklabels(["1.0", "0.8", "0.6", "0.4", "0.2", "0.0"])
# Axis labels
ax.set_xlabel("Standard Deviation", labelpad=20)
# Correlation text label on the plot
ax.text(
0.85,
0.7,
"Correlation",
ha="center",
rotation_mode="anchor",
rotation=-45,
transform=ax.transAxes,
)
# Set size of ticks and labels if provided
# if size_props:
# tick_size = size_props.get("ticks", 10)
# label_size = size_props.get("label", 12)
# ax.tick_params(axis="both", labelsize=tick_size)
# # X-label
# for label in ax.xaxis.get_label():
# label.set_size(label_size)
# # We might want to set radial labels if any,
# # but let's keep it minimal.
if size_props:
tick_size = size_props.get("ticks", 10)
label_size = size_props.get("label", 12)
ax.tick_params(axis="both", labelsize=tick_size)
ax.xaxis.label.set_size(label_size)
# Legend and title
ax.legend(loc="upper right")
ax.set_title(title or "Taylor Diagram")
fig.tight_layout()
# Save or show figure
if savefig:
fig.savefig(savefig, bbox_inches="tight")
plt.close(fig)
else:
plt.show()
return ax
[docs]
@validate_params(
{
"reference": ["array-like"],
"names": [str, "array-like", None],
"acov": [StrOptions({"default", "half_circle"}), None],
"zero_location": [
StrOptions({"N", "NE", "E", "S", "SW", "W", "NW", "SE"})
],
"direction": [Integral],
}
)
def plot_taylor_diagram_in(
*y_preds,
reference,
names=None,
acov=None,
zero_location="E",
direction=-1,
only_points=False,
ref_color="red",
draw_ref_arc=True,
angle_to_corr=True,
marker="o",
corr_steps=6,
cmap="viridis",
shading="auto",
shading_res=300,
radial_strategy=None,
norm_c=False,
norm_range=None,
cbar="off",
fig_size=None,
title=None,
savefig=None,
ax=None,
):
r"""Plot Taylor Diagram with background color map.
Generates a Taylor Diagram comparing predictions to a reference,
featuring a background color map encoding correlation or other
metrics based on the chosen strategy.
The diagram uses polar coordinates where the radial axis
represents the standard deviation (:math:`\sigma_p`) of each
prediction, and the angular axis represents the correlation
(:math:`\rho`) with the reference, typically via the angle
:math:`\theta = \arccos(\rho)`.
Parameters
----------
*y_preds : array-like
One or more 1D prediction arrays (e.g., model outputs).
Each array must have the same length as `reference`.
Multi-dimensional inputs are flattened internally.
reference : array-like
The 1D reference (observed) array of shape :math:`(n,)`.
Must have the same length as each array in `*y_preds`.
names : list of str or None, optional
Labels for each prediction array in `*y_preds`. Must match
the number of predictions if provided. If ``None``, defaults
like "Pred 1", "Pred 2" are used.
acov : {'default', 'half_circle'}, optional
Angular coverage of the diagram:
- ``'default'``: Spans :math:`\pi` (180 degrees).
- ``'half_circle'``: Spans :math:`\pi/2` (90 degrees).
If ``None``, defaults to ``'half_circle'``.
zero_location : {'N','NE','E','S','SW','W','NW','SE'}, optional
Position corresponding to perfect correlation (:math:`\rho=1`).
Default is ``'E'``.
direction : int, optional
Rotation direction for increasing angles (correlation).
``1`` for counter-clockwise, ``-1`` for clockwise.
Default is ``-1``.
only_points : bool, optional
If ``True``, plot only markers for predictions, omitting
radial lines from the origin. Default is ``False``.
ref_color : str, optional
Color for the reference standard deviation marker (arc or
line/point). Default is ``'red'``.
draw_ref_arc : bool, optional
If ``True`` (default), draw reference std dev as an arc.
If ``False``, draw as a radial line/point at angle zero.
angle_to_corr : bool, optional
If ``True`` (default), label the angular axis with
correlation values (:math:`\rho`). If ``False``, label with
degrees.
marker : str, optional
Marker style for prediction points (e.g., 'o', '^', 's').
Default is ``'o'``.
corr_steps : int, optional
Number of correlation ticks (0 to 1) when `angle_to_corr`
is ``True``. Default is 6.
cmap : str, optional
Colormap name for the background mesh (e.g., 'viridis').
Default is ``'viridis'``.
shading : {'auto', 'gouraud', 'nearest'}, optional
Shading method for the background mesh (`pcolormesh`).
Default is ``'auto'``.
shading_res : int, optional
Resolution for the background mesh grid. Default is 300.
radial_strategy : {'convergence', 'norm_r', 'performance'}, optional
Strategy for calculating background color values:
- ``'convergence'``: Color maps to correlation :math:`\cos(\theta)`.
- ``'norm_r'``: Color maps to normalized radius (std dev).
- ``'performance'``: Color highlights region near the best
performing input model.
If ``None``, defaults to ``'performance'``. `'rwf'` and
`'center_focus'` are not supported here.
norm_c : bool, optional
If ``True``, normalize background color values using
`norm_range`. Default is ``False``.
norm_range: tuple of (float, float), optional
Range `(min, max)` for background color normalization when
`norm_c` is ``True``. Default is ``(0, 1)``.
cbar : bool or {'off'}, optional
Control colorbar display for the background mesh.
``'off'`` or ``False`` hides it, ``True`` shows it.
Default is ``'off'``.
fig_size : tuple of (float, float), optional
Figure size in inches ``(width, height)``. Default is ``(10, 8)``.
title : str, optional
Title of the diagram. Default is ``"Taylor Diagram"``.
savefig : str or None, optional
Path to save the figure (e.g., ``"diagram.png"``). If `None`,
the figure is displayed interactively. Default is `None`.
Returns
-------
ax : matplotlib.axes.Axes
The Matplotlib Axes object containing the Taylor Diagram.
*(Note: Original code may not explicitly return ax.)*
Raises
------
ValueError
If input arrays have inconsistent lengths or if invalid
parameter options are provided (e.g., for `acov`,
`zero_location`, `radial_strategy`).
TypeError
If non-numeric data is encountered in input arrays.
Notes
-----
The Taylor diagram [1]_ displays standard deviation (:math:`\sigma_p`)
and correlation (:math:`\rho`) relative to a reference (:math:`r`)
with standard deviation :math:`\sigma_r`.
1. **Correlation** (:math:`\rho`):
.. math::
\rho = \frac{\mathrm{Cov}(p, r)}{\sigma_p \sigma_r}
2. **Standard Deviation** (:math:`\sigma_p`):
.. math::
\sigma_p = \sqrt{\frac{1}{n}
\sum_{i=1}^n (p_i - \bar{p})^2}
The plot uses polar coordinates where radius is :math:`\sigma_p`
and angle is :math:`\theta = \arccos(\rho)`. The distance from a
plotted point to the reference point represents the centered RMS
difference.
See Also
--------
numpy.corrcoef : Compute correlation coefficients.
numpy.std : Compute standard deviation.
kdiagram.plot.evaluation.taylor_diagram : Flexible version accepting
stats or arrays.
kdiagram.plot.evaluation.plot_taylor_diagram : Basic version without
background shading.
References
----------
.. [1] Taylor, K. E. (2001). Summarizing multiple aspects of model
performance in a single diagram. *Journal of Geophysical
Research*, 106(D7), 7183-7192.
Examples
--------
>>> import numpy as np
>>> from kdiagram.plot.evaluation import plot_taylor_diagram_in
>>> np.random.seed(42)
>>> reference = np.random.normal(0, 1, 100)
>>> y_preds = [
... reference + np.random.normal(0, 0.3, 100),
... reference * 0.9 + np.random.normal(0, 0.8, 100)
... ]
>>> # ax = plot_taylor_diagram_in( # Capture axis if returned
>>> plot_taylor_diagram_in(
... *y_preds,
... reference=reference,
... names=['Model A', 'Model B'],
... acov='half_circle',
... zero_location='N',
... direction=1,
... fig_size=(8, 8),
... cbar=True,
... radial_strategy='convergence'
... )
>>> # Plot is shown if savefig is None
"""
# Flatten the reference and predictions
reference = np.ravel(reference)
y_preds = [np.ravel(yp) for yp in y_preds]
n = reference.size
for p in y_preds:
if p.size != n:
raise ValueError(
"All predictions and reference must be the same length."
)
# correlation & stdev
corrs = [np.corrcoef(p, reference)[0, 1] for p in y_preds]
stds = [np.std(p) for p in y_preds]
ref_std = np.std(reference)
# Setup figure & polar axis
if ax is None:
fig = plt.figure(figsize=fig_size or (10, 8))
ax = fig.add_subplot(111, polar=True)
else:
fig = ax.figure
# Decide coverage
acov = acov or "half_circle"
if acov == "half_circle":
angle_max = np.pi / 2
else:
angle_max = np.pi
# radial limit
rad_limit = max(max(stds), ref_std) * 1.2
# Create a mesh for background
theta_grid = np.linspace(0, angle_max, shading_res)
r_grid = np.linspace(0, rad_limit, shading_res)
TH, RR = np.meshgrid(theta_grid, r_grid)
if radial_strategy == "convergence":
# correlation => cos(TH)
# correlation = cos(TH) if half or full circle
# (when angle=0 => correlation=1, angle= pi/2 => corr=0, angle= pi => corr=-1)
CC = np.cos(TH) # from 1..-1 or 1..0 depending on coverage
elif radial_strategy == "norm_r":
CC = RR / rad_limit # Normalizes r to range [0, 1]
else:
if radial_strategy in {"rwf", "center_focus"}:
warnings.warn(
f"'{radial_strategy}' is not available in the current"
" plot. Consider using `gofast.plot.taylor_diagram`"
" for better support. Alternatively, choose from"
" 'convergence', 'norm_r', or 'performance'."
" Defaulting to 'performance' visualization.",
stacklevel=2,
)
# Fallback to performance
best_idx = np.argmax(corrs)
std_best = stds[best_idx]
corr_best = corrs[best_idx]
theta_best = np.arccos(corr_best)
std_diff = (RR - std_best) ** 2
theta_diff = (TH - theta_best) ** 2
CC = np.exp(-std_diff / 0.05) * np.exp(-theta_diff / 0.05)
# Define color values based on radial distance (normalized)
if norm_c:
if norm_range is None:
norm_range = (0, 1)
norm_range = validate_length_range(
norm_range, param_name="Normalized Range"
)
CC = minmax_scaler(CC, feature_range=norm_range)
# plot background
# Turn off grid to avoid the deprecation error
ax.grid(False)
c = ax.pcolormesh(
TH,
RR,
CC,
cmap=cmap,
shading=shading,
vmin=-1 if angle_max == np.pi else 0,
vmax=1,
)
ax.grid(True, which="both")
# convert each correlation to an angle
angles = np.arccos(corrs)
radii = stds
# pick distinct colors
colors = plt.cm.Set1(np.linspace(0, 1, len(y_preds)))
names = columns_manager(names, empty_as_none=False)
# plot predictions
for i, (ang, rd) in enumerate(zip(angles, radii)):
label = names[i] if (names and i < len(names)) else f"Pred {i + 1}"
if not only_points:
ax.plot([ang, ang], [0, rd], color=colors[i], lw=2, alpha=0.8)
ax.plot(ang, rd, marker=marker, color=colors[i], label=label)
# reference arc
if draw_ref_arc:
arc_t = np.linspace(0, angle_max, 300)
ax.plot(
arc_t, [ref_std] * 300, color=ref_color, lw=2, label="Reference"
)
else:
ax.plot(
[0, 0], [0, ref_std], color=ref_color, lw=2, label="Reference"
)
ax.plot(0, ref_std, marker=marker, color=ref_color)
# set coverage
ax.set_thetamax(np.degrees(angle_max))
# direction
if direction not in (-1, 1):
warnings.warn("direction must be -1 or 1; using 1.", stacklevel=2)
direction = 1
ax.set_theta_direction(direction)
ax.set_theta_zero_location(zero_location)
# Use coordinates and positions to avoid overlapping
CORR_POS = TDG_DIRECTIONS[str(direction)]["CORR_POS"]
STD_POS = TDG_DIRECTIONS[str(direction)]["STD_POS"]
corr_pos = CORR_POS.get(zero_location)[0]
corr_kw = CORR_POS.get(zero_location)[1]
std_pos = STD_POS.get(zero_location)[0]
std_kw = STD_POS.get(zero_location)[1]
# angle => corr labels
if angle_to_corr:
corr_ticks = np.linspace(0, 1, corr_steps)
angles_deg = np.degrees(np.arccos(corr_ticks))
ax.set_thetagrids(
angles_deg, labels=[f"{ct:.2f}" for ct in corr_ticks]
)
ax.text(
*corr_pos,
"Correlation",
ha="center",
va="bottom",
transform=ax.transAxes,
**corr_kw,
)
ax.text(
*std_pos,
"Standard Deviation",
ha="center",
va="bottom",
transform=ax.transAxes,
**std_kw,
)
else:
ax.text(
*corr_pos,
"Angle (degrees)",
ha="center",
va="bottom",
transform=ax.transAxes,
**corr_kw,
)
ax.text(
*std_pos,
"Standard Deviation",
ha="center",
va="bottom",
transform=ax.transAxes,
**std_kw,
)
ax.set_ylim(0, rad_limit)
ax.set_rlabel_position(15)
title = title or "Taylor Diagram"
ax.set_title(title, pad=60)
ax.legend(loc="upper right", bbox_to_anchor=(1.2, 1.1))
if cbar not in ["off", False]:
fig.colorbar(c, ax=ax, pad=0.1, label="Correlation")
fig.tight_layout()
if savefig:
fig.savefig(savefig, bbox_inches="tight")
plt.close(fig)
else:
plt.show()
return ax
[docs]
@validate_params(
{
"reference": ["array-like"],
"names": [str, "array-like", None],
"acov": [StrOptions({"default", "half_circle"})],
"zero_location": [
StrOptions({"N", "NE", "E", "S", "SW", "W", "NW", "SE"})
],
"direction": [Integral],
}
)
def plot_taylor_diagram(
*y_preds: np.ndarray,
reference: np.ndarray,
names: list[str] | None = None,
acov: str = "half_circle",
zero_location: str = "W",
direction: int = -1,
only_points: bool = False,
ref_color: str = "red",
draw_ref_arc: bool = True,
angle_to_corr: bool = True,
marker="o",
corr_steps=6,
fig_size: tuple[int, int] | None = None,
title: str | None = None,
savefig: str | None = None,
ax: Axes | None = None,
):
r"""Plot a standard Taylor Diagram.
Graphically summarizes how closely a set of predictions match
observations (reference). The diagram displays the correlation
coefficient and standard deviation of each prediction relative
to the reference data [1]_.
Parameters
----------
*y_preds : array-like
Variable number of 1D prediction arrays (e.g., model
outputs). Each must have the same length as `reference`.
reference : array-like
1D array of reference (observation) data. Must have the
same length as each prediction array in `*y_preds`.
names : list of str, optional
Labels for each prediction series in `*y_preds`. Must match
the number of predictions if provided. If ``None``,
defaults like "Prediction 1", "Prediction 2" are used.
acov : {'default', 'half_circle'}, default: "half_circle"
Determines the angular coverage (correlation range) of the
plot:
- ``'default'``: Spans 180 degrees (:math:`\pi`), covering
correlations from -1 to 1 (if applicable).
- ``'half_circle'``: Spans 90 degrees (:math:`\pi/2`),
covering positive correlations from 0 to 1.
zero_location : {'N','NE','E','S','SW','W','NW','SE'}, default: 'W'
Specifies the position on the polar plot corresponding to
perfect correlation (:math:`\rho=1`, angle=0). For example,
``'N'`` (North) is top, ``'E'`` (East) is right, ``'W'``
(West) is left.
**Effects:**
- **Positioning:** Changes the orientation of the diagram by rotating
the zero-degree line.
- **Interpretation:** Influences how the angular coordinates
correspond to correlation values.
**Example:**
- Setting `zero_location='N'` places the zero-degree correlation
at the top of the plot.
- Setting `zero_location='E'` places it on the right side.
direction : {1, -1}, default: -1
Direction of increasing angle (decreasing correlation).
``1`` for counter-clockwise, ``-1`` for clockwise.
**Effects:**
- **Angle Progression:** Dictates whether the angles move clockwise or
counter-clockwise from the zero location.
- **Visual Interpretation:** Affects the layout of the correlations
on the diagram.
**Example:**
- `direction=1`: Correlation angles increase counter-clockwise, which
might align with standard mathematical conventions.
- `direction=-1`: Correlation angles increase clockwise, which might
be preferred for specific visualization standards.
only_points : bool, default: False
If ``True``, only plot markers for predictions. If ``False``,
also draw radial lines from the origin to each marker.
ref_color : str, default: 'red'
Color for the reference standard deviation marker (arc or
point/line). Accepts any valid matplotlib color format.
draw_ref_arc : bool, default: True
If ``True``, show the reference standard deviation as an arc.
If ``False``, show as a marker/line at angle zero.
angle_to_corr : bool, default: True
If ``True``, label the angular axis (theta) with correlation
values (:math:`\rho`). If ``False``, label with degrees.
marker : str, default: 'o'
Marker style for the prediction points (e.g., 'o', 's', '^').
corr_steps : int, default: 6
Number of ticks (intervals) between 0 and 1 correlation when
`angle_to_corr` is ``True``.
fig_size : tuple of (float, float), optional
Figure size in inches ``(width, height)``. If `None`,
defaults to ``(10, 8)``.
title : str, optional
Title for the diagram. If `None`, defaults to
"Taylor Diagram".
savefig : str or None, optional
Path to save the figure (e.g., ``"diagram.png"``). If `None`,
the figure is displayed interactively. Default is `None`.
Returns
-------
ax : matplotlib.axes.Axes
The Matplotlib Axes object containing the Taylor Diagram.
*(Note: Original code implementation may need `return ax` added.)*
Raises
------
ValueError
If input arrays have inconsistent lengths or if invalid
parameter options are provided for `acov`, `zero_location`,
or `direction`.
AssertionError
If length check inside the function fails (redundant with
ValueError from decorator likely).
TypeError
If non-numeric data is encountered in input arrays.
Notes
-----
Taylor diagrams visually assess model performance relative to
observations based on three statistics: the correlation
coefficient (:math:`R`), the standard deviation (:math:`\sigma`),
and the centered root-mean-square difference (RMSD).
The relationship is based on the law of cosines:
.. math::
RMSD^2 = \sigma_p^2 + \sigma_r^2 - 2\sigma_p \sigma_r R
where :math:`\sigma_p` and :math:`\sigma_r` are the standard
deviations of the prediction and reference, respectively.
On the diagram:
- Radial distance = Standard deviation (:math:`\sigma_p`)
- Angle = Correlation (:math:`\arccos(R)`)
- Distance to reference point = Centered RMSD
See Also
--------
numpy.corrcoef : Compute correlation coefficients.
numpy.std : Compute standard deviation.
kdiagram.plot.evaluation.taylor_diagram : Flexible version.
kdiagram.plot.evaluation.plot_taylor_diagram_in : Version with
background shading.
References
----------
.. [1] Taylor, K. E. (2001). Summarizing multiple aspects of
model performance in a single diagram. *Journal of
Geophysical Research*, 106(D7), 7183-7192.
Examples
--------
>>> import numpy as np
>>> from kdiagram.plot.evaluation import plot_taylor_diagram
>>> np.random.seed(101)
>>> reference = np.random.normal(0, 1.0, 100)
>>> y_preds = [
... reference * 0.8 + np.random.normal(0, 0.4, 100), # Model A
... reference * 0.5 + np.random.normal(0, 1.1, 100) # Model B
... ]
>>> # ax = plot_taylor_diagram( # Capture axis if returned
>>> plot_taylor_diagram(
... *y_preds,
... reference=reference,
... names=['Model A', 'Model B'],
... acov='half_circle',
... zero_location='W', # Corr=1 on West axis
... fig_size=(9, 7)
... )
"""
# Convert inputs to 1D numpy arrays
y_preds = [np.asarray(pred).flatten() for pred in y_preds]
reference = np.asarray(reference).flatten()
# Check consistency of lengths
assert all(pred.size == reference.size for pred in y_preds), (
"All predictions and the reference must be of the same length."
)
# Compute correlation and std dev for each prediction
correlations = [np.corrcoef(pred, reference)[0, 1] for pred in y_preds]
standard_deviations = [np.std(pred) for pred in y_preds]
reference_std = np.std(reference)
# standard_deviations= normalize_array(
# standard_deviations, normalize = "auto", method="01"
# )
# correlations= normalize_array(
# correlations, normalize = "auto", method="01"
# )
# Create figure and polar subplot
if ax is None:
fig = plt.figure(figsize=fig_size or (10, 8))
ax = fig.add_subplot(111, polar=True)
else:
fig = ax.figure
# Convert correlation to angles (in radians)
# angle = arccos(corr), so perfect correlation = 0 rad,
# zero correlation = pi/2 rad, negative correlation = > pi/2, etc.
angles = np.arccos(correlations)
radii = standard_deviations
# Plot each prediction
# Use a color cycle so lines/points are more distinguishable
colors = plt.cm.Set1(np.linspace(0, 1, len(y_preds)))
for i, (angle, radius) in enumerate(zip(angles, radii)):
label = (
names[i] if (names and i < len(names)) else f"Prediction {i + 1}"
)
if not only_points:
# Draw the radial line from origin to the point
ax.plot(
[angle, angle], [0, radius], color=colors[i], lw=2, alpha=0.8
)
ax.plot(angle, radius, marker, color=colors[i], label=label)
# Draw the reference as a red arc if requested
# This arc will have radius = reference_std
if draw_ref_arc:
if acov == "half_circle":
theta_arc = np.linspace(0, np.pi / 2, 300)
else:
theta_arc = np.linspace(0, np.pi, 300)
ax.plot(
theta_arc,
[reference_std] * len(theta_arc),
color=ref_color,
lw=2,
label="Reference",
)
else:
# If not drawing the arc, revert to a radial line as fallback
ax.plot(
[0, 0],
[0, reference_std],
color=ref_color,
lw=2,
label="Reference",
)
ax.plot(0, reference_std, marker, color=ref_color)
# Set coverage (max angle)
if acov == "half_circle":
ax.set_thetamax(90) # degrees
else:
ax.set_thetamax(180) # degrees (default)
# Set direction (1=counterclockwise, -1=clockwise)
if direction not in [-1, 1]:
warnings.warn(
"direction should be either 1 (CCW) or -1 (CW). "
f"Got {direction}. Resetting to 1 (CCW).",
stacklevel=2,
)
direction = 1
ax.set_theta_direction(direction)
ax.set_theta_zero_location(zero_location)
CORR_POS = TDG_DIRECTIONS[str(direction)]["CORR_POS"]
STD_POS = TDG_DIRECTIONS[str(direction)]["STD_POS"]
corr_pos = CORR_POS.get(zero_location)[0]
corr_kw = CORR_POS.get(zero_location)[1]
std_pos = STD_POS.get(zero_location)[0]
std_kw = STD_POS.get(zero_location)[1]
# Replace angle ticks with correlation values if requested
if angle_to_corr:
# We'll map correlation ticks [0..1] -> angle via arccos
# e.g. 1 -> 0 rad, 0 -> pi/2 or pi, depending on coverage
# Just pick some correlation tick steps
corr_ticks = np.linspace(0, 1, corr_steps) # 0, 0.2, 0.4, 0.6, 0.8, 1
angle_ticks = np.degrees(np.arccos(corr_ticks))
# In half-circle mode, correlation from 0..1 fits in 0..pi/2,
# in default mode, 0..1 fits in 0..pi. This still works generally.
ax.set_thetagrids(
angle_ticks, labels=[f"{ct:.2f}" for ct in corr_ticks]
)
# We can label this dimension as 'Correlation'
ax.set_ylabel("") # remove default 0.5, 1.06
ax.text(
*corr_pos,
"Correlation",
ha="center",
va="center",
transform=ax.transAxes,
**corr_kw,
)
ax.text(
*std_pos,
"Standard Deviation",
ha="center",
va="center",
transform=ax.transAxes,
**std_kw,
)
else:
# Keep angle as degrees
ax.set_ylabel("") # remove default
ax.text(
*corr_pos,
"Angle (degrees)",
ha="center",
va="center",
transform=ax.transAxes,
)
# Adjust radial label (std dev)
# This tries to reduce label overlap
ax.set_rlabel_position(22.5)
ax.set_title(title or "Taylor Diagram", pad=60) # 50
# ax.set_xlabel('Standard Deviation', labelpad=15)
ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.05))
# plt.subplots_adjust(top=0.8)
fig.tight_layout()
if savefig:
fig.savefig(savefig, bbox_inches="tight")
plt.close(fig)
else:
plt.show()
return ax