Skip to content

Commit 2299c1d

Browse files
Split Quantile Mapping into Quantile Mapping and Detrended Quantile Mapping (#34)
1 parent 8ad7bb9 commit 2299c1d

File tree

5 files changed

+198
-82
lines changed

5 files changed

+198
-82
lines changed

.pylintrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ ignored-parents=
282282
max-args=10
283283

284284
# Maximum number of attributes for a class (see R0902).
285-
max-attributes=7
285+
max-attributes=20
286286

287287
# Maximum number of boolean expressions in an if statement (see R0916).
288288
max-bool-expr=5
@@ -426,7 +426,8 @@ disable=raw-checker-failed,
426426
line-too-long,
427427
import-error,
428428
consider-using-with,
429-
c-extension-no-member
429+
c-extension-no-member,
430+
too-many-return-statements
430431

431432

432433
# Enable the message, report, category or checker with the given id(s). You can

cmethods/__init__.py

Lines changed: 154 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -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(

docs/src/cmethods.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Classes and Functions
55
.. currentmodule:: cmethods
66

77
.. autoclass:: CMethods
8-
:members: linear_scaling, variance_scaling, delta_method, quantile_mapping, quantile_delta_mapping, adjust_3d
8+
:members: linear_scaling, variance_scaling, delta_method, quantile_mapping, detrended_quantile_mapping, quantile_delta_mapping, adjust_3d
99

1010

1111
Some helpful additional methods

docs/src/introduction.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ The following bias correction techniques are available:
5050

5151
Distribution-based techniques:
5252
* Quantile Mapping :func:`cmethods.CMethods.quantile_mapping`
53+
* Detrended Quantile Mapping :func:`cmethods.CMethods.detrended_quantile_mapping`
5354
* Quantile Delta Mapping :func:`cmethods.CMethods.quantile_delta_mapping`
5455

5556
All of these methods are intended to be applied on 1-dimensional time-series climate data.

0 commit comments

Comments
 (0)