Skip to content

Commit 7fe5d3d

Browse files
fixed inconsistent return types; added testing
1 parent 9ae86de commit 7fe5d3d

File tree

7 files changed

+221
-73
lines changed

7 files changed

+221
-73
lines changed

README.md

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
11
# Bias-Adjustment-Python
22

3-
[![Generic badge](https://img.shields.io/badge/license-MIT-green.svg)](https://shields.io/)
4-
[![Generic badge](https://img.shields.io/badge/python-3.7+-blue.svg)](https://shields.io/)
3+
<div style="text-align: center">
4+
55
[![GitHub](https://badgen.net/badge/icon/github?icon=github&label)](https://github.com/btschwertfeger/Bias-Adjustment-Python)
6+
[![Generic badge](https://img.shields.io/badge/python-3.7+-blue.svg)](https://shields.io/)
7+
[![Generic badge](https://img.shields.io/badge/license-MIT-green.svg)](https://shields.io/)
8+
[![Downloads](https://pepy.tech/badge/python-cmethods)](https://pepy.tech/project/python-cmethods)
9+
[![Downloads](https://pepy.tech/badge/python-cmethods/month)](https://pepy.tech/project/python-cmethods)
610

11+
</div>
712
Collection of different scale- and distribution-based bias adjustment techniques for climatic research. (see `examples.ipynb` for help)
813

914
Bias adjustment procedures in Python are very slow, so they should not be used on large data sets.
1015
A C++ implementation that works way faster can be found here: [https://github.com/btschwertfeger/Bias-Adjustment-Cpp](https://github.com/btschwertfeger/Bias-Adjustment-Cpp).
11-
____
16+
17+
---
18+
1219
## Available methods:
20+
1321
- Linear Scaling (additive and multiplicative)
1422
- Variance Scaling (additive)
1523
- Delta (Change) Method (additive and multiplicative)
1624
- Quantile Mapping (additive)
1725
- Detrended Quantile Mapping (additive and multiplicative)
1826
- Quantile Delta Mapping (additive and multuplicative)
1927

20-
____
28+
---
29+
2130
## Usage
2231

2332
### Installation
33+
2434
```bash
2535
python3 -m pip install python-cmethods
2636
```
37+
2738
### Import and application
28-
```python
39+
40+
```python
2941
import xarray as xr
3042
from cmethods.CMethods import CMethods
3143
cm = CMethods()
@@ -41,25 +53,27 @@ ls_result = cm.linear_scaling(
4153
kind = '+' # *
4254
)
4355

44-
qdm_result = cm.adjust_2d( # 2d = 2 spatial and 1 time dimension
56+
qdm_result = cm.adjust_3d( # 3d = 2 spatial and 1 time dimension
4557
method = 'quantile_delta_mapping',
4658
obs = obsh['tas'],
4759
simh = simh['tas'],
4860
simp = simp['tas'],
4961
n_quaniles = 1000,
5062
kind = '+' # *
5163
)
52-
# * to calculate the relative rather than the absolute change,
64+
# * to calculate the relative rather than the absolute change,
5365
# '*' can be used instead of '+' (this is prefered when adjusting
5466
# ratio based variables like precipitation)
5567
```
5668

57-
____
69+
---
70+
5871
## Examples (see repository on [GitHub](https://github.com/btschwertfeger/Bias-Adjustment-Python))
5972

6073
`/examples/examples.ipynb`: Notebook containing different methods and plots
6174

6275
`/examples/do_bias_correction.py`: Example script for adjusting climate data
76+
6377
```bash
6478
python3 do_bias_correction.py \
6579
--obs input_data/obs.nc \
@@ -69,17 +83,21 @@ python3 do_bias_correction.py \
6983
--variable tas \
7084
--unit '°C' \
7185
--group time.month \
72-
--kind +
86+
--kind +
7387
```
7488

7589
- Linear and variance, as well as delta change method require `--group time.month` as argument.
7690
- Adjustment methods that apply changes in distributional biasses (QM, QDM, DQM; EQM, ...) need the `--nquantiles` argument set to some integer.
7791
- Data sets should have the same spatial resolutions.
78-
____
92+
93+
---
94+
7995
## Notes:
96+
8097
- Computation in Python takes some time, so this is only for demonstration. When adjusting large datasets, its best to the C++ implementation mentioned above.
8198

8299
## References
100+
83101
- Schwertfeger, Benjamin Thomas (2022) The influence of bias corrections on variability, distribution, and correlation of temperatures in comparison to observed and modeled climate data in Europe (https://epic.awi.de/id/eprint/56689/)
84102
- Linear Scaling and Variance Scaling based on: Teutschbein, Claudia and Seibert, Jan (2012) Bias correction of regional climate model simulations for hydrological climate-change impact studies: Review and evaluation of different methods (https://doi.org/10.1016/j.jhydrol.2012.05.052)
85103
- Delta Method based on: Beyer, R. and Krapp, M. and Manica, A.: An empirical evaluation of bias correction methods for palaeoclimate simulations (https://doi.org/10.5194/cp-16-1493-2020)

cmethods/CMethods.py

+35-28
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
simp = data to correct (predicted simulated data) ($T_{sim,p}$) \
2424
\
2525
\
26-
F = Cummulative Distribution Function \
26+
F = Cumulative Distribution Function \
2727
\mu = mean \
2828
\sigma = standard deviation \
2929
i = index \
@@ -46,9 +46,14 @@ def __init__(self, method: str, available_methods: list):
4646

4747
SCALING_METHODS = ['linear_scaling', 'variance_scaling', 'delta_method']
4848
DISTRIBUTION_METHODS = ['quantile_mapping', 'quantile_delta_mapping']
49+
4950
CUSTOM_METHODS = SCALING_METHODS + DISTRIBUTION_METHODS
51+
5052
METHODS = CUSTOM_METHODS #+ XCLIM_SDBA_METHODS
5153

54+
ADDITIVE = ['+', 'add']
55+
MULTIPLICATIVE = ['*', 'mult']
56+
5257
def __init__(self):
5358
pass
5459

@@ -68,7 +73,7 @@ def get_function(cls, method: str):
6873
else: raise UnknownMethodError(method, cls.METHODS)
6974

7075
@classmethod
71-
def adjust_2d(cls,
76+
def adjust_3d(cls,
7277
method: str,
7378
obs: xr.core.dataarray.DataArray,
7479
simh: xr.core.dataarray.DataArray,
@@ -104,9 +109,9 @@ def adjust_2d(cls,
104109
> simh = xarray.open_dataset('path/to/simulated/data.nc')
105110
> obs = xarray.open_dataset('path/to/observed/data.nc')
106111
> simp = xarray.open_dataset('path/to/simulated_future/data.nc')
107-
> variable = 'temperature'
112+
> variable = 'tas'
108113
109-
> adjusted_data = CMethods().adjust_2d(
114+
> adjusted_data = CMethods().adjust_3d(
110115
method = 'quantile_delta_mapping',
111116
obs = obs[variable],
112117
simh = simh[variable],
@@ -121,7 +126,7 @@ def adjust_2d(cls,
121126
simh = simh.transpose('lat', 'lon', 'time')
122127
simp = simp.transpose('lat', 'lon', 'time')
123128

124-
if group == None and method in SCALING_METHODS: group = 'time.month'
129+
if group == None and method in cls.SCALING_METHODS: group = 'time.month'
125130

126131
result = simp.copy(deep=True).load()
127132
len_lat, len_lon = len(obs.lat), len(obs.lon)
@@ -160,7 +165,7 @@ def adjust_2d(cls,
160165
@classmethod
161166
def pool_adjust(cls, params) -> xr.core.dataarray.DataArray:
162167
''' Adjustment along longitude for one specific latitude
163-
used by cls.adjust_2d as callbackfunction for multiprocessing.Pool
168+
used by cls.adjust_3d as callbackfunction for multiprocessing.Pool
164169
'''
165170

166171
method = params['method']
@@ -249,7 +254,7 @@ def linear_scaling(cls,
249254
simh (xarray.core.dataarray.DataArray): simulated historical Data
250255
simp (xarray.core.dataarray.DataArray): future simulated Data
251256
group (str): [optional] Group / Period (e.g.: 'time.month')
252-
kind (str): '+' or '*', default: '+'
257+
kind (str): [optional] '+' or '*', default: '+'
253258
254259
----- R E T U R N -----
255260
@@ -259,13 +264,13 @@ def linear_scaling(cls,
259264
> obs = xarray.open_dataset('path/to/observed/data.nc')
260265
> simh = xarray.open_dataset('path/to/simulated/data.nc')
261266
> simp = xarray.open_dataset('path/to/predicted/data.nc')
262-
> variable = 'temperature'
267+
> variable = 'tas'
263268
264269
> result = CMethods().linear_scaling(
265270
> obs=obs[variable],
266271
> simh=simh[variable],
267272
> simp=simp[variable],
268-
> group='time.month'
273+
> group='time.month' # optional
269274
>)
270275
271276
----- E Q U A T I O N S -----
@@ -281,11 +286,10 @@ def linear_scaling(cls,
281286
https://doi.org/10.1016/j.jhydrol.2012.05.052
282287
283288
'''
284-
285289
if group != None: return cls.grouped_correction(method='linear_scaling', obs=obs, simh=simh, simp=simp, group=group, kind=kind, **kwargs)
286290
else:
287-
if kind == '+': return np.array(simp) + (np.nanmean(obs) - np.nanmean(simh)) # Eq. 1
288-
elif kind == '*': return np.array(simp) * (np.nanmean(obs) / np.nanmean(simh)) # Eq. 2
291+
if kind in cls.ADDITIVE: return np.array(simp) + (np.nanmean(obs) - np.nanmean(simh)) # Eq. 1
292+
elif kind in cls.MULTIPLICATIVE: return np.array(simp) * (np.nanmean(obs) / np.nanmean(simh)) # Eq. 2
289293
else: raise ValueError('Scaling type invalid. Valid options for param kind: "+" and "*"')
290294

291295
# ? -----========= V A R I A N C E - S C A L I N G =========------
@@ -306,7 +310,7 @@ def variance_scaling(cls,
306310
simh (xarray.core.dataarray.DataArray): simulated historical Data
307311
simp (xarray.core.dataarray.DataArray): future simulated Data
308312
group (str): [optional] Group / Period (e.g.: 'time.month')
309-
kind (str): '+' or '*', default: '+'
313+
kind (str): '+' or '*', default: '+' # '*' is not implemented
310314
311315
----- R E T U R N -----
312316
@@ -316,7 +320,7 @@ def variance_scaling(cls,
316320
> obs = xarray.open_dataset('path/to/observed/data.nc')
317321
> simh = xarray.open_dataset('path/to/simulated/data.nc')
318322
> simp = xarray.open_dataset('path/to/predicted/data.nc')
319-
> variable = 'temperature'
323+
> variable = 'tas'
320324
321325
> result = CMethods().variance_scaling(obs=obs[variable], simh=simh[variable], simp=simp[variable] group='time.dayofyear')
322326
@@ -341,8 +345,8 @@ def variance_scaling(cls,
341345
'''
342346
if group != None: return cls.grouped_correction(method='variance_scaling', obs=obs, simh=simh, simp=simp, group=group, kind=kind, **kwargs)
343347
else:
344-
LS_simh = cls.linear_scaling(obs, simh, simh) # Eq. 1
345-
LS_simp = cls.linear_scaling(obs, simh, simp) # Eq. 2
348+
LS_simh = cls.linear_scaling(obs, simh, simh, group=None) # Eq. 1
349+
LS_simp = cls.linear_scaling(obs, simh, simp, group=None) # Eq. 2
346350

347351
VS_1_simh = LS_simh - np.nanmean(LS_simh) # Eq. 3
348352
VS_1_simp = LS_simp - np.nanmean(LS_simp) # Eq. 4
@@ -379,7 +383,7 @@ def delta_method(cls,
379383
> obs = xarray.open_dataset('path/to/observed/data.nc')
380384
> simh = xarray.open_dataset('path/to/simulated/data.nc')
381385
> simp = xarray.open_dataset('path/to/predicted/data.nc')
382-
> variable = 'temperature'
386+
> variable = 'tas'
383387
384388
> result = CMethods().delta_method(obs=obs[variable], simh=simh[variable], group='time.month')
385389
@@ -399,8 +403,8 @@ def delta_method(cls,
399403
'''
400404
if group != None: return cls.grouped_correction(method='delta_method', obs=obs, simh=simh, simp=simp, group=group, kind=kind, **kwargs)
401405
else:
402-
if kind == '+': return np.array(obs) + (np.nanmean(simp) - np.nanmean(simh)) # Eq. 1
403-
elif kind == '*': return np.array(obs) * (np.nanmean(simp) / np.nanmean(simh)) # Eq. 2
406+
if kind in cls.ADDITIVE: return np.array(obs) + (np.nanmean(simp) - np.nanmean(simh)) # Eq. 1
407+
elif kind in cls.MULTIPLICATIVE: return np.array(obs) * (np.nanmean(simp) / np.nanmean(simh)) # Eq. 2
404408
else: raise ValueError(f'{kind} not implemented! Use "+" or "*" instead.')
405409

406410

@@ -468,7 +472,7 @@ def quantile_mapping(cls,
468472
cdf_obs = cls.get_cdf(obs, xbins)
469473
cdf_simh = cls.get_cdf(simh, xbins)
470474

471-
if kwargs.get('detrended', False) or kind in [ '*', 'mult' ]:
475+
if kwargs.get('detrended', False) or kind in cls.MULTIPLICATIVE:
472476
'''detrended => shift mean of $X_{sim,p}$ to range of $X_{sim,h}$ to adjust extremes'''
473477
for month, idxs in res.groupby('time.month').groups.items():
474478
m_simh, m_simp = [], []
@@ -481,7 +485,7 @@ def quantile_mapping(cls,
481485
m_simh_mean = np.nanmean(m_simh)
482486
m_simp_mean = np.nanmean(m_simp)
483487

484-
if kind == '+':
488+
if kind in cls.ADDITIVE:
485489
epsilon = np.interp(m_simp - m_simp_mean + m_simh_mean, xbins, cdf_simh) # Eq. 1
486490
X = cls.get_inverse_of_cdf(cdf_obs, epsilon, xbins) + m_simp_mean - m_simh_mean # Eq. 1
487491
else:
@@ -490,7 +494,7 @@ def quantile_mapping(cls,
490494
#x = cm.get_inverse_of_cdf(cdf_obs, epsilon, xbins) * (m_simp_mean / m_simh_mean)
491495
for i, idx in enumerate(idxs): res.values[idx] = X[i]
492496
return res
493-
elif kind in [ '+', 'add' ]: # additive, no detrend
497+
elif kind in cls.ADDITIVE: # additive, no detrend
494498
epsilon = np.interp(simp, xbins, cdf_simh) # Eq. 1
495499
res.values = cls.get_inverse_of_cdf(cdf_obs, epsilon, xbins) # Eq. 1
496500
return res
@@ -527,7 +531,7 @@ def empirical_quantile_mapping(cls,
527531
> obs = xarray.open_dataset('path/to/observed/data.nc')
528532
> simh = xarray.open_dataset('path/to/simulated/data.nc')
529533
> simp = xarray.open_dataset('path/to/future/data.nc')
530-
> variable = 'temperature'
534+
> variable = 'tas'
531535
> result = CMethods().empirical_quantile_mapping(
532536
> obs=obs[variable],
533537
> simh=simh[variable],
@@ -613,7 +617,8 @@ def quantile_delta_mapping(cls,
613617
'''
614618

615619
if group != None: return cls.grouped_correction(method='quantile_delta_mapping', obs=obs, simh=simh, simp=simp, group=group, n_quantiles=n_quantiles, kind=kind, **kwargs)
616-
elif kind == '+':
620+
elif kind in cls.ADDITIVE:
621+
res = simp.copy(deep=True)
617622
obs, simh, simp = np.array(obs), np.array(simh), np.array(simp) # to achieve higher accuracy
618623
global_max = max(np.amax(obs), np.amax(simh))
619624
global_min = min(np.amin(obs), np.amin(simh))
@@ -628,9 +633,11 @@ def quantile_delta_mapping(cls,
628633
epsilon = np.interp(simp, xbins, cdf_simp) # Eq. 1.1
629634
QDM1 = cls.get_inverse_of_cdf(cdf_obs, epsilon, xbins) # Eq. 1.2
630635
delta = simp - cls.get_inverse_of_cdf(cdf_simh, epsilon, xbins) # Eq. 1.3
631-
return QDM1 + delta # Eq. 1.4
636+
res.values = QDM1 + delta # Eq. 1.4
637+
return res
632638

633-
elif kind == '*':
639+
elif kind in cls.MULTIPLICATIVE:
640+
res = simp.copy(deep=True)
634641
obs, simh, simp = np.array(obs), np.array(simh), np.array(simp)
635642
global_max = max(np.amax(obs), np.amax(simh))
636643
wide = global_max / n_quantiles
@@ -643,8 +650,8 @@ def quantile_delta_mapping(cls,
643650
epsilon = np.interp(simp, xbins, cdf_simp) # Eq. 1.1
644651
QDM1 = cls.get_inverse_of_cdf(cdf_obs, epsilon, xbins) # Eq. 1.2
645652
delta = simp / cls.get_inverse_of_cdf(cdf_simh, epsilon, xbins) # Eq. 2.3
646-
return QDM1 * delta # Eq. 2.4
647-
653+
res.values = QDM1 * delta # Eq. 2.4
654+
return res
648655
else: raise ValueError(f'Unknown kind {kind}!')
649656

650657
# * -----========= G E N E R A L =========------

cmethods/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION = (0,5,1)
1+
VERSION = (0,5,3)
22
__version__ = '.'.join(map(str, VERSION))

0 commit comments

Comments
 (0)