@@ -58,6 +58,7 @@ class CMethods:
5858
5959 Distribution-based techniques:
6060 * Quantile Mapping :func:`cmethods.CMethods.quantile_mapping`
61+ * Detrended Quantile Mapping :func:`cmethods.CMethods.detrended_quantile_mapping`
6162 * Quantile Delta Mapping :func:`cmethods.CMethods.quantile_delta_mapping`
6263
6364 Except for the Variance Scaling all methods can be applied on both, stochastic and non-stochastic
@@ -73,7 +74,11 @@ class CMethods:
7374 """
7475
7576 SCALING_METHODS = ["linear_scaling" , "variance_scaling" , "delta_method" ]
76- DISTRIBUTION_METHODS = ["quantile_mapping" , "quantile_delta_mapping" ]
77+ DISTRIBUTION_METHODS = [
78+ "quantile_mapping" ,
79+ "detrended_quantile_mapping" ,
80+ "quantile_delta_mapping" ,
81+ ]
7782
7883 CUSTOM_METHODS = SCALING_METHODS + DISTRIBUTION_METHODS
7984 METHODS = CUSTOM_METHODS
@@ -127,6 +132,8 @@ def get_function(cls, method: str):
127132 return cls .delta_method
128133 if method == "quantile_mapping" :
129134 return cls .quantile_mapping
135+ if method == "detrended_quantile_mapping" :
136+ return cls .detrended_quantile_mapping
130137 if method == "empirical_quantile_mapping" :
131138 return cls .empirical_quantile_mapping
132139 if method == "quantile_delta_mapping" :
@@ -717,7 +724,6 @@ def quantile_mapping(
717724 simp : xr .core .dataarray .DataArray ,
718725 n_quantiles : int ,
719726 kind : str = "+" ,
720- detrended : bool = False ,
721727 ** kwargs ,
722728 ) -> np .array :
723729 r"""
@@ -735,14 +741,10 @@ def quantile_mapping(
735741 Precipitation by Quantile Mapping: How Well Do Methods Preserve Changes in Quantiles
736742 and Extremes?"* (https://doi.org/10.1175/JCLI-D-14-00754.1).
737743
738- Since the regular Quantile Mapping is bounded to the value range of the modeled data of
739- the control period, the *Detrended* Quantile Mapping approach can be used by setting the
740- ``detrended`` argument to ``True``. Detrending means, that the values of :math:`X_{sim,p}`
741- are shifted to the value range of :math:`X_{sim,h}` before the Quantile Mapping is applied.
742- After the Quantile Mapping was applied, the mean is shifted back. Since it does not make sens
743- to take the whole mean to rescale the data, the month-dependent long-term mean is used.
744+ The regular Quantile Mapping is bounded to the value range of the modeled data
745+ of the control period. To avoid this, the Detrended Quantile Mapping can be used.
744746
745- In the following the equations of Alex J. Cannon (2015) are shown and explained (without detrending) :
747+ In the following the equations of Alex J. Cannon (2015) are shown and explained:
746748
747749 **Additive**:
748750
@@ -795,10 +797,7 @@ def quantile_mapping(
795797 :param kind: The kind of the correction, additive for non-stochastic and multiplicative
796798 for stochastic climate variables, defaults to ``+``
797799 :type kind: str, optional
798- :param detrended: If the extremes should be respected by applying month-dependent
799- detrending before and after applying the Quantile Mapping
800- :type detrended: bool, optional
801- :param val_min: Lower boundary for interpolation (only if ``kind="*"``, default: ``0``)
800+ :param val_min: Lower boundary for interpolation (only if ``kind="*"``, default: ``0.0``)
802801 :type val_min: float, optional
803802 :param val_max: Upper boundary for interpolation (only if ``kind="*"``, default: ``None``)
804803 :type val_max: float, optional
@@ -827,7 +826,7 @@ def quantile_mapping(
827826 ... n_quantiles=250
828827 ... )
829828 """
830- obs , simh , simp_ = np .array (obs ), np .array (simh ), np .array (simp )
829+ obs , simh , simp = np .array (obs ), np .array (simh ), np .array (simp )
831830
832831 global_max = max (np .amax (obs ), np .amax (simh ))
833832 global_min = min (np .amin (obs ), np .amin (simh ))
@@ -837,53 +836,13 @@ def quantile_mapping(
837836 cdf_obs = cls .get_cdf (obs , xbins )
838837 cdf_simh = cls .get_cdf (simh , xbins )
839838
840- if detrended :
841- # detrended => shift mean of $X_{sim,p}$ to range of $X_{sim,h}$ to adjust extremes
842- res = np .zeros (len (simp .values ))
843- for _ , idxs in simp .groupby ("time.month" ).groups .items ():
844- # detrended by long-term month
845- m_simh , m_simp = [], []
846- for idx in idxs :
847- m_simh .append (simh [idx ])
848- m_simp .append (simp [idx ])
849-
850- m_simh = np .array (m_simh )
851- m_simp = np .array (m_simp )
852- m_simh_mean = np .nanmean (m_simh )
853- m_simp_mean = np .nanmean (m_simp )
854-
855- if kind in cls .ADDITIVE :
856- epsilon = np .interp (
857- m_simp - m_simp_mean + m_simh_mean , xbins , cdf_simh
858- ) # Eq. 1
859- X = (
860- cls .get_inverse_of_cdf (cdf_obs , epsilon , xbins )
861- + m_simp_mean
862- - m_simh_mean
863- ) # Eq. 1
864-
865- elif kind in cls .MULTIPLICATIVE :
866- epsilon = np .interp ( # Eq. 2
867- cls .ensure_devidable ((m_simh_mean * m_simp ), m_simp_mean ),
868- xbins ,
869- cdf_simh ,
870- left = kwargs .get ("val_min" , 0.0 ),
871- right = kwargs .get ("val_max" , None ),
872- )
873- X = np .interp (epsilon , cdf_obs , xbins ) * (
874- cls .ensure_devidable (m_simp_mean , m_simh_mean )
875- ) # Eq. 2
876- for i , idx in enumerate (idxs ):
877- res [idx ] = X [i ]
878- return res
879-
880839 if kind in cls .ADDITIVE :
881- epsilon = np .interp (simp_ , xbins , cdf_simh ) # Eq. 1
840+ epsilon = np .interp (simp , xbins , cdf_simh ) # Eq. 1
882841 return cls .get_inverse_of_cdf (cdf_obs , epsilon , xbins ) # Eq. 1
883842
884843 if kind in cls .MULTIPLICATIVE :
885844 epsilon = np .interp ( # Eq. 2
886- simp_ ,
845+ simp ,
887846 xbins ,
888847 cdf_simh ,
889848 left = kwargs .get ("val_min" , 0.0 ),
@@ -895,6 +854,145 @@ def quantile_mapping(
895854 f"{ kind } for quantile_mapping is not available. Use '+' or '*' instead."
896855 )
897856
857+ @classmethod
858+ def detrended_quantile_mapping (
859+ cls ,
860+ obs : xr .core .dataarray .DataArray ,
861+ simh : xr .core .dataarray .DataArray ,
862+ simp : xr .core .dataarray .DataArray ,
863+ n_quantiles : int ,
864+ kind : str = "+" ,
865+ ** kwargs ,
866+ ) -> np .array :
867+ r"""
868+ The Detrended Quantile Mapping bias correction technique can be used to minimize distributional
869+ biases between modeled and observed time-series climate data like the regular Quantile Mapping.
870+ Detrending means, that the values of :math:`X_{sim,p}` are shifted to the value range of
871+ :math:`X_{sim,h}` before the regular Quantile Mapping is applied.
872+ After the Quantile Mapping was applied, the mean is shifted back. Since it does not make sens
873+ to take the whole mean to rescale the data, the month-dependent long-term mean is used.
874+
875+ This method must be applied on a 1-dimensional data set i.e., there is only one
876+ time-series passed for each of ``obs``, ``simh``, and ``simp``. Also this method requires
877+ that the time series are groupable by ``time.month``.
878+
879+ The Detrended Quantile Mapping technique implemented here is based on the equations of
880+ Alex J. Cannon and Stephen R. Sobie and Trevor Q. Murdock (2015) *"Bias Correction of GCM
881+ Precipitation by Quantile Mapping: How Well Do Methods Preserve Changes in Quantiles
882+ and Extremes?"* (https://doi.org/10.1175/JCLI-D-14-00754.1).
883+
884+ In the following the equations of Alex J. Cannon (2015) are shown and explained (without detrending):
885+
886+ **Additive**:
887+
888+ .. math::
889+
890+ X^{*QM}_{sim,p}(i) = F^{-1}_{obs,h} \left\{F_{sim,h}\left[X_{sim,p}(i)\right]\right\}
891+
892+
893+ **Multiplicative**:
894+
895+ .. math::
896+
897+ X^{*QM}_{sim,p}(i) = F^{-1}_{obs,h}\Biggl\{F_{sim,h}\left[\frac{\mu{X_{sim,h}} \cdot \mu{X_{sim,p}(i)}}{\mu{X_{sim,p}(i)}}\right]\Biggr\}\frac{\mu{X_{sim,p}(i)}}{\mu{X_{sim,h}}}
898+
899+
900+ :param obs: The reference data set of the control period
901+ (in most cases the observational data)
902+ :type obs: xr.core.dataarray.DataArray
903+ :param simh: The modeled data of the control period
904+ :type simh: xr.core.dataarray.DataArray
905+ :param simp: The modeled data of the scenario period (this is the data set
906+ on which the bias correction takes action)
907+ :type simp: xr.core.dataarray.DataArray
908+ :param n_quantiles: Number of quantiles to respect/use
909+ :type n_quantiles: int
910+ :param kind: The kind of the correction, additive for non-stochastic and multiplicative
911+ for stochastic climate variables, defaults to ``+``
912+ :type kind: str, optional
913+ :param val_min: Lower boundary for interpolation (only if ``kind="*"``, default: ``0.0``)
914+ :type val_min: float, optional
915+ :param val_max: Upper boundary for interpolation (only if ``kind="*"``, default: ``None``)
916+ :type val_max: float, optional
917+ :raises NotImplementedError: If the kind is not in (``+``, ``*``, ``add``, ``mult``)
918+ :return: The bias-corrected time series
919+ :rtype: np.array
920+
921+ .. code-block:: python
922+ :linenos:
923+ :caption: Example: Quantile Mapping
924+
925+ >>> import xarray as xr
926+ >>> from cmethods import CMethods as cm
927+
928+ >>> # Note: The data sets must contain the dimension "time"
929+ >>> # for the respective variable.
930+ >>> obsh = xr.open_dataset("path/to/reference_data-control_period.nc")
931+ >>> simh = xr.open_dataset("path/to/modeled_data-control_period.nc")
932+ >>> simp = xr.open_dataset("path/to/the_dataset_to_adjust-scenario_period.nc")
933+ >>> variable = "tas" # temperatures
934+
935+ >>> qm_adjusted = cm.detrended_quantile_mapping(
936+ ... obs=obs[variable],
937+ ... simh=simh[variable],
938+ ... simp=simp[variable],
939+ ... n_quantiles=250
940+ ... )
941+ """
942+ if kind not in cls .MULTIPLICATIVE and kind not in cls .ADDITIVE :
943+ raise NotImplementedError (
944+ f"{ kind } for detrended_quantile_mapping is not available. Use '+' or '*' instead."
945+ )
946+
947+ obs , simh = np .array (obs ), np .array (simh )
948+
949+ global_max = max (np .amax (obs ), np .amax (simh ))
950+ global_min = min (np .amin (obs ), np .amin (simh ))
951+ wide = abs (global_max - global_min ) / n_quantiles
952+ xbins = np .arange (global_min , global_max + wide , wide )
953+
954+ cdf_obs = cls .get_cdf (obs , xbins )
955+ cdf_simh = cls .get_cdf (simh , xbins )
956+
957+ # detrended => shift mean of $X_{sim,p}$ to range of $X_{sim,h}$ to adjust extremes
958+ res = np .zeros (len (simp .values ))
959+ for _ , idxs in simp .groupby ("time.month" ).groups .items ():
960+ # detrended by long-term month
961+ m_simh , m_simp = [], []
962+ for idx in idxs :
963+ m_simh .append (simh [idx ])
964+ m_simp .append (simp [idx ])
965+
966+ m_simh = np .array (m_simh )
967+ m_simp = np .array (m_simp )
968+ m_simh_mean = np .nanmean (m_simh )
969+ m_simp_mean = np .nanmean (m_simp )
970+
971+ if kind in cls .ADDITIVE :
972+ epsilon = np .interp (
973+ m_simp - m_simp_mean + m_simh_mean , xbins , cdf_simh
974+ ) # Eq. 1
975+ X = (
976+ cls .get_inverse_of_cdf (cdf_obs , epsilon , xbins )
977+ + m_simp_mean
978+ - m_simh_mean
979+ ) # Eq. 1
980+
981+ elif kind in cls .MULTIPLICATIVE :
982+ epsilon = np .interp ( # Eq. 2
983+ cls .ensure_devidable ((m_simh_mean * m_simp ), m_simp_mean ),
984+ xbins ,
985+ cdf_simh ,
986+ left = kwargs .get ("val_min" , 0.0 ),
987+ right = kwargs .get ("val_max" , None ),
988+ )
989+ X = np .interp (epsilon , cdf_obs , xbins ) * cls .ensure_devidable (
990+ m_simp_mean , m_simh_mean
991+ ) # Eq. 2
992+ for i , idx in enumerate (idxs ):
993+ res [idx ] = X [i ]
994+ return res
995+
898996 # ? -----========= E M P I R I C A L - Q U A N T I L E - M A P P I N G =========------
899997 @classmethod
900998 def empirical_quantile_mapping (
0 commit comments