Source code for de.kge

# -*- coding: utf-8 -*-

"""
de.kge
~~~~~~~~~~~
Kling-Gupta efficiency measure. The efficiency measure can be
visualized in a polar plot which facilitates decomposing the metric terms (bias
error vs. variability error vs. timing error)
:2021 by Robin Schwemmle.
:license: GNU GPLv3, see LICENSE for more details.
"""

import numpy as np
from matplotlib import cm
import matplotlib.ticker as mticker
import scipy as sp
import matplotlib
import seaborn as sns
matplotlib.use("agg")
import matplotlib.pyplot as plt  # noqa: E402

# RunTimeWarning will not be displayed (division by zeros or NaN values)
np.seterr(divide="ignore", invalid="ignore")

# controlling figure aesthetics
sns.set_style("ticks", {"xtick.major.size": 8, "ytick.major.size": 8})


[docs]def calc_temp_cor(obs, sim, r="pearson"): """ Calculate temporal correlation between observed and simulated time series. Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series r : str, optional Either Spearman correlation coefficient ('spearman') or Pearson correlation coefficient ('pearson') can be used to describe the temporalcorrelation. The default is to calculate the Pearson correlation. Returns ---------- temp_cor : float correlation between observed and simulated time series Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_temp_cor(obs, sim) 0.8940281850583509 """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if r == "spearman": r = sp.stats.spearmanr(obs, sim) temp_cor = r[0] if np.isnan(temp_cor): temp_cor = 0 elif r == "pearson": r = sp.stats.pearsonr(obs, sim) temp_cor = r[0] if np.isnan(temp_cor): temp_cor = 0 return temp_cor
[docs]def calc_kge_beta(obs, sim): r""" Calculate the beta term of Kling-Gupta-Efficiency (KGE). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array Returns ---------- kge_beta : float alpha value Notes ---------- .. math:: \beta = \frac{\mu_{sim}}{\mu_{obs}} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_kge_beta(obs, sim) 1.0980392156862746 References ---------- Gupta, H. V., Kling, H., Yilmaz, K. K., and Martinez, G. F.: Decomposition of the mean squared error and NSE performance criteria: Implications for improving hydrological modelling, Journal of Hydrology, 377, 80-91, 10.1016/j.jhydrol.2009.08.003, 2009. Kling, H., Fuchs, M., and Paulin, M.: Runoff conditions in the upper Danube basin under an ensemble of climate change scenarios, Journal of Hydrology, 424-425, 264-277, 10.1016/j.jhydrol.2012.01.011, 2012. Pool, S., Vis, M., and Seibert, J.: Evaluating model performance: towards a non-parametric variant of the Kling-Gupta efficiency, Hydrological Sciences Journal, 63, 1941-1953, 10.1080/02626667.2018.1552002, 2018. """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") # calculate alpha term obs_mean = np.mean(obs) sim_mean = np.mean(sim) kge_beta = sim_mean / obs_mean return kge_beta
[docs]def calc_kge_alpha(obs, sim): r""" Calculate the alpha term of the Kling-Gupta-Efficiency (KGE). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series Returns ---------- kge_alpha : float alpha value Notes ---------- .. math:: \alpha = \frac{\sigma_{sim}}{\sigma_{obs}} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> kge.calc_kge_alpha(obs, sim) 1.2812057455166919 References ---------- Gupta, H. V., Kling, H., Yilmaz, K. K., and Martinez, G. F.: Decomposition of the mean squared error and NSE performance criteria: Implications for improving hydrological modelling, Journal of Hydrology, 377, 80-91, 10.1016/j.jhydrol.2009.08.003, 2009. Kling, H., Fuchs, M., and Paulin, M.: Runoff conditions in the upper Danube basin under an ensemble of climate change scenarios, Journal of Hydrology, 424-425, 264-277, 10.1016/j.jhydrol.2012.01.011, 2012. Pool, S., Vis, M., and Seibert, J.: Evaluating model performance: towards a non-parametric variant of the Kling-Gupta efficiency, Hydrological Sciences Journal, 63, 1941-1953, 10.1080/02626667.2018.1552002, 2018. """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") obs_std = np.std(obs) sim_std = np.std(sim) kge_alpha = sim_std / obs_std return kge_alpha
[docs]def calc_kge_gamma(obs, sim): r""" Calculate the gamma term of Kling-Gupta-Efficiency (KGE). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array Returns ---------- kge_gamma : float gamma value Notes ---------- .. math:: \gamma = \frac{CV_{sim}}{CV_{obs}} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> kge.calc_kge_gamma(obs, sim) 1.166812375381273 References ---------- Gupta, H. V., Kling, H., Yilmaz, K. K., and Martinez, G. F.: Decomposition of the mean squared error and NSE performance criteria: Implications for improving hydrological modelling, Journal of Hydrology, 377, 80-91, 10.1016/j.jhydrol.2009.08.003, 2009. Kling, H., Fuchs, M., and Paulin, M.: Runoff conditions in the upper Danube basin under an ensemble of climate change scenarios, Journal of Hydrology, 424-425, 264-277, 10.1016/j.jhydrol.2012.01.011, 2012. Pool, S., Vis, M., and Seibert, J.: Evaluating model performance: towards a non-parametric variant of the Kling-Gupta efficiency, Hydrological Sciences Journal, 63, 1941-1953, 10.1080/02626667.2018.1552002, 2018. """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") obs_mean = np.mean(obs) sim_mean = np.mean(sim) obs_std = np.std(obs) sim_std = np.std(sim) obs_cv = obs_std / obs_mean sim_cv = sim_std / sim_mean kge_gamma = sim_cv / obs_cv return kge_gamma
[docs]def calc_kge(obs, sim, r="pearson", var="std"): r""" Calculate Kling-Gupta-Efficiency (KGE). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array r : str, optional Either Spearman correlation coefficient ('spearman'; Pool et al. 2018) or Pearson correlation coefficient ('pearson'; Gupta et al. 2009) can be used to describe the temporal correlation. The default is to calculate the Pearson correlation. var : str, optional Either coefficient of variation ('cv'; Kling et al. 2012) or standard deviation ('std'; Gupta et al. 2009, Pool et al. 2018) to describe the gamma term. The default is to calculate the standard deviation. Returns ---------- eff : float Kling-Gupta-Efficiency Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> kge.calc_kge(obs, sim) 0.683901305466148 Notes ---------- .. math:: KGE = 1 - \sqrt{(\beta - 1)^2 + (\alpha - 1)^2 + (r - 1)^2} KGE = 1 - \sqrt{(\frac{\mu_{sim}}{\mu_{obs}} - 1)^2 + (\frac{\sigma_{sim}}{\sigma_{obs}} - 1)^2 + (r - 1)^2} KGE = 1 - \sqrt{(\beta - 1)^2 + (\gamma - 1)^2 + (r - 1)^2} KGE = 1 - \sqrt{(\frac{\mu_{sim}}{\mu_{obs}} - 1)^2 + (\frac{CV_{sim}}{CV_{obs}} - 1)^2 + (r - 1)^2} References ---------- Gupta, H. V., Kling, H., Yilmaz, K. K., and Martinez, G. F.: Decomposition of the mean squared error and NSE performance criteria: Implications for improving hydrological modelling, Journal of Hydrology, 377, 80-91, 10.1016/j.jhydrol.2009.08.003, 2009. Kling, H., Fuchs, M., and Paulin, M.: Runoff conditions in the upper Danube basin under an ensemble of climate change scenarios, Journal of Hydrology, 424-425, 264-277, 10.1016/j.jhydrol.2012.01.011, 2012. Pool, S., Vis, M., and Seibert, J.: Evaluating model performance: towards a non-parametric variant of the Kling-Gupta efficiency, Hydrological Sciences Journal, 63, 1941-1953, 10.1080/02626667.2018.1552002, 2018. """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") # calculate alpha term obs_mean = np.mean(obs) sim_mean = np.mean(sim) kge_beta = sim_mean / obs_mean # calculate KGE with gamma term if var == "cv": kge_gamma = calc_kge_gamma(obs, sim) temp_cor = calc_temp_cor(obs, sim, r=r) eff = 1 - np.sqrt( (kge_beta - 1) ** 2 + (kge_gamma - 1) ** 2 + (temp_cor - 1) ** 2 ) # calculate KGE with beta term elif var == "std": kge_alpha = calc_kge_alpha(obs, sim) temp_cor = calc_temp_cor(obs, sim, r=r) eff = 1 - np.sqrt( (kge_beta - 1) ** 2 + (kge_alpha - 1) ** 2 + (temp_cor - 1) ** 2 ) return eff
def calc_kge_skill(obs, sim, bench, r="pearson", var="std"): r""" Calculate the Kling-Gupta-Efficiency skill score ($KGE_{skill}$). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array bench : (N,)array_like Benchmarked time series as 1-D array r : str, optional Either Spearman correlation coefficient ('spearman'; Pool et al. 2018) or Pearson correlation coefficient ('pearson'; Gupta et al. 2009) can be used to describe the temporal correlation. The default calculates the Pearson correlation. var : str, optional Either coefficient of variation ('cv'; Kling et al. 2012) or standard deviation ('std'; Gupta et al. 2009, Pool et al. 2018) to describe the gamma term. The default calculates the standard deviation. Returns ---------- eff : float Kling-Gupta-Efficiency measure normalized by mean flow benchmark Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> bench = np.array([1, 1.1, 1.15, 1.15, 1.1, 1]) >>> kge.calc_kge_skill(obs, sim, bench) 0.7790394993537985 Notes ---------- .. math:: KGE_{skill} = \frac{KGE - KGE{bench}}{1 - KGE{bench}} """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if len(obs) != len(bench): raise AssertionError("Arrays are not of equal length!") # calculate KGE with gamma term if var == "cv": eff_kge = calc_kge(obs, sim, var="cv") eff_bench = calc_kge(obs, bench) eff = (eff_kge - (eff_bench)) / (1 - (eff_bench)) # calculate KGE with beta term elif var == "std": eff_kge = calc_kge(obs, sim, var="std") eff_bench = calc_kge(obs, bench) eff = (eff_kge - (eff_bench)) / (1 - (eff_bench)) return eff
[docs]def polar_plot(obs, sim, r="pearson", var="std"): r""" Polar plot of Diagnostic efficiency (KGE) for a single evaluation. Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array r : str, optional Either Spearman correlation coefficient ('spearman') or Pearson correlation coefficient ('pearson') can be used to describe the temporal correlation. The default calculates the Pearson correlation. var : str, optional Either coefficient of variation ('cv') or standard deviation ('std') to describe the gamma term. The default calculates the standard deviation. Returns ---------- fig : Figure diagnostic polar plot Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> kge.diag_polar_plot_kge(obs, sim) """ # calculate beta term obs_mean = np.mean(obs) sim_mean = np.mean(sim) kge_beta = sim_mean / obs_mean # calculate gamma term if var == "cv": obs_std = np.std(obs) sim_std = np.std(sim) obs_cv = obs_std / obs_mean sim_cv = sim_std / sim_mean kge_gamma = sim_cv / obs_cv kge_r = calc_temp_cor(obs, sim, r=r) eff = 1 - np.sqrt( (kge_beta - 1) ** 2 + (kge_gamma - 1) ** 2 + (kge_r - 1) ** 2 ) # convert to radians # (y, x) Trigonometric inverse tangent phi = np.arctan2(kge_beta - 1, kge_gamma - 1) # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) rgba_color = cm.plasma_r(norm(kge_r)) delta = 0.01 # for spacing # determine axis limits if eff >= 0: ax_lim = 0.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(0, 1, 0.2) elif eff < 0 and eff >= -1: ax_lim = 1.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-1, 1, 0.2) elif eff >= -2 and eff < -1: ax_lim = 2.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-2, 1, 0.2) elif eff <= -2: raise AssertionError("Value of 'KGE' too low for visualization!", eff) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (1, np.deg2rad(45)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(135)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(225)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(315)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of KGE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=1, fmt="%1.1f", colors="dimgrey") # diagnose the error if eff < 0.9: ax.annotate( "", xytext=(0, 1), xy=(phi, eff), arrowprops=dict(facecolor=rgba_color) ) elif eff >= 0.9: ax.scatter(phi, eff, color=rgba_color) ax.set_rticks([]) # turn defalut ticks off ax.set_rmin(1) if eff >= 0: ax.set_rmax(0) elif eff <= 0: ax.set_rmax(-ax_lim + 0.2) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ax.text( -0.09, 0.5, "Variability underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"($\alpha$ - 1) < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.09, 0.5, "Variability overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"($\alpha$ - 1) > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.09, "Mean underestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"($\beta$ - 1) < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.09, "Mean overestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"($\beta$ - 1) > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") # calculate alpha term elif var == "std": obs_std = np.std(obs) sim_std = np.std(sim) kge_alpha = sim_std / obs_std kge_r = calc_temp_cor(obs, sim, r=r) eff = 1 - np.sqrt((kge_beta - 1) ** 2 + (kge_alpha - 1) ** 2 + (kge_r - 1) ** 2) # convert to radians # (y, x) Trigonometric inverse tangent phi = np.arctan2(kge_beta - 1, kge_alpha - 1) # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) rgba_color = cm.plasma_r(norm(kge_r)) delta = 0.01 # for spacing # determine axis limits if eff >= 0: ax_lim = 0.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(0, 1, 0.2) elif eff < 0 and eff >= -1: ax_lim = 1.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-1, 1, 0.2) elif eff >= -2 and eff < -1: ax_lim = 2.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-2, 1, 0.2) elif eff <= -2: raise AssertionError("Value of 'KGE' is too low for visualization!", eff) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (1, np.deg2rad(45)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(135)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(225)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(315)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of KGE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=1, fmt="%1.1f", colors="dimgrey") # diagnose the error if eff < 0.9: ax.annotate( "", xytext=(0, 1), xy=(phi, eff), arrowprops=dict(facecolor=rgba_color) ) elif eff >= 0.9: ax.scatter(phi, eff, color=rgba_color) ax.set_rticks([]) # turn defalut ticks off ax.set_rmin(1) if eff >= 0: ax.set_rmax(0) elif eff < 0: ax.set_rmax(-ax_lim + 0.2) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ax.text( -0.09, 0.5, "Variability underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"($\alpha$ - 1) < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.09, 0.5, "Variability overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"($\alpha$ - 1) > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.09, "Mean underestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"($\beta$ - 1) < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.09, "Mean overestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"($\beta$ - 1) > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") return fig
def polar_plot_multi( kge_beta, alpha_or_gamma, kge_r, eff_kge, var="std", extended=False ): r""" Polar plot for Kling-Gupta efficiency (KGE) for multiple evaluations. Parameters ---------- kge_beta: (N,)array_like KGE beta as 1-D array alpha_or_gamma : (N,)array_like KGE alpha or KGE gamma as 1-D array kge_r : (N,)array_like KGE r as 1-D array eff_kge : (N,)array_like KGE as 1-D array var : str, optional Either coefficient of variation ('cv') or standard deviation ('std') to describe the gamma term or alpha term, respectively. The default is 'std'. extended : boolean, optional If True, density plot is displayed. In addtion, the density plot is displayed besides the polar plot. The default is, that only the diagnostic polar plot is displayed. Returns ---------- fig : Figure Returns a single figure if extended=False and two figures if extended=True. Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> beta = np.array([1.1, 1.15, 1.2, 1.1, 1.05, 1.15]) >>> alpha = np.array([1.15, 1.1, 1.2, 1.1, 1.1, 1.2]) >>> r = np.array([0.9, 0.85, 0.8, 0.9, 0.85, 0.9]) >>> eff_kge = np.array([0.79, 0.77, 0.65, 0.83, 0.81, 0.73]) >>> kge.diag_polar_plot_multi(beta, alpha, r, eff_kge) """ eff_min = np.min(eff_kge) ll_kge_beta = kge_beta.tolist() ll_ag = alpha_or_gamma.tolist() ll_kge_r = kge_r.tolist() ll_eff = eff_kge.tolist() # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) delta = 0.01 # for spacing # determine axis limits if eff_min >= 0: ax_lim = 0.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(0, 1, 0.2) elif eff_min < 0 and eff_min >= -1: ax_lim = 1.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-1, 1, 0.2) elif eff_min >= -2 and eff_min < -1: ax_lim = 2.2 yy = np.arange(-ax_lim, 1.01, delta)[::-1] c_levels = np.arange(-2, 1, 0.2) elif eff_min <= -2: raise ValueError("Some values of 'KGE' are too low for visualization!", eff_min) if var == "std": var_lab = "alpha" if var == "cv": var_lab = "gamma" else: var_lab = "alpha" len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot if not extended: fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (1, np.deg2rad(45)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(135)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(225)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(315)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of KGE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # loop over each data point for (b, ag, r, eff) in zip(ll_kge_beta, ll_ag, ll_kge_r, ll_eff): ang = np.arctan2(b - 1, ag - 1) # convert temporal correlation to color rgba_color = cm.plasma_r(norm(r)) c = ax.scatter(ang, eff, color=rgba_color) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ax.text( -0.09, 0.5, "Variability underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"($\%s$ - 1) < 0" % (var_lab), va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.09, 0.5, "Variability overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"($\%s$ - 1) > 0" % (var_lab), va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.09, "Mean underestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"($\beta$ - 1) < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.09, "Mean overestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"($\beta$ - 1) > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.set_rticks([]) # turn default ticks off ax.set_rmin(1) if eff_min >= 0: ax.set_rmax(0) elif eff_min < 0: ax.set_rmax(-ax_lim + 0.2) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") return fig elif extended: fig = plt.figure(figsize=(6, 3), constrained_layout=True) gs = fig.add_gridspec(1, 2) ax = fig.add_subplot(gs[0, 0], projection="polar") ax1 = fig.add_axes([0.64, 0.3, 0.33, 0.33], frameon=True) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (1, np.deg2rad(45)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(135)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(225)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (1, np.deg2rad(315)), (1, np.min(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of KGE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # loop over each data point for (b, ag, r, eff) in zip(ll_kge_beta, ll_ag, ll_kge_r, ll_eff): ang = np.arctan2(b - 1, ag - 1) # convert temporal correlation to color rgba_color = cm.plasma_r(norm(r)) c = ax.scatter(ang, eff, color=rgba_color) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ax.text( -0.09, 0.5, "Variability underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"($\%s$ - 1) < 0" % (var_lab), va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.09, 0.5, "Variability overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"($\%s$ - 1) > 0" % (var_lab), va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.09, "Mean underestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"($\beta$ - 1) < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.09, "Mean overestimation", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"($\beta$ - 1) > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", r"0$^{\circ}$ (360$^{\circ}$)", "", ""]) ax.set_rticks([]) # turn default ticks off ax.set_rmin(1) if eff_min > 0: ax.set_rmax(ax_lim) elif eff_min <= 0: ax.set_rmax(-ax_lim) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") # convert to degrees phi = np.arctan2(kge_beta - 1, alpha_or_gamma - 1) diag_deg = (phi * (180 / np.pi)) + 135 diag_deg[diag_deg < 0] = 360 - diag_deg[diag_deg < 0] # 1-D density plot g = sns.kdeplot(y=diag_deg, color="k", ax=ax1) ax1.set_xticks([0, 90, 180, 270, 360]) ax1.set_xlim(0, 360) ax1.set_ylim(0,) ax1.set(ylabel="Density", xlabel=r"[$^\circ$]") # 2-D density plot r_colors = cm.plasma_r(norm(kge_r)) g = sns.jointplot( x=diag_deg, y=eff_kge, kind="kde", zorder=1, n_levels=20, cmap="Greys", shade_lowest=False, marginal_kws={"color": "k", "shade": False}, ).plot_joint(plt.scatter, c=r_colors, alpha=0.4, zorder=2) g.set_axis_labels(r"[$^\circ$]", r"KGE [-]") g.ax_joint.set_xticks([0, 90, 180, 270, 360]) g.ax_joint.set_xlim(0, 360) g.ax_joint.set_ylim(-ax_lim, 1) g.ax_marg_x.set_xticks([0, 90, 180, 270, 360]) kde_data = g.ax_marg_y.get_lines()[0].get_data() kde_xx = kde_data[0] kde_yy = kde_data[1] norm = matplotlib.colors.Normalize(vmin=-ax_lim, vmax=1.0) colors = cm.Reds_r(norm(kde_yy)) npts = len(kde_xx) for i in range(npts - 1): g.ax_marg_y.fill_betweenx( [kde_yy[i], kde_yy[i + 1]], [kde_xx[i], kde_xx[i + 1]], color=colors[i] ) g.fig.tight_layout() return fig, g.fig