Skip to content

Commit

Permalink
Fix: QuantileDeltaMapping not working for seasonal grouping (#1716)
Browse files Browse the repository at this point in the history
### Pull Request Checklist:
    
- This PR fixes #1704 
- [x] Added `test_seasonal` test to the QDM suit
- [x] CHANGES.rst has been updated (with summary of main changes)
- [x] Link to issue (:issue:`number`) and pull request (:pull:`number`)
has been added

### What kind of change does this PR introduce?

* In sdba/utils.py `interp_on_quantiles` seasons are now cast to
integers to ensure we can add_cyclic_bounds. For this, I also had to
overwrite the get_index function of the `Grouper` class. On a first
class, I think that makes sense since in any case it doesn't make much
sense to interp a string variable.

### Does this PR introduce a breaking change?
Hopefully not.
  • Loading branch information
aulemahal authored Apr 19, 2024
2 parents b1cdbef + 5fb5f94 commit 93539ba
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Bug fixes
* Fixed and adapted ``time_bnds`` to the newest xarray. (:pull:`1700`).
* Fixed "agreement fraction" in ``robustness_fractions`` to distinguish between negative change and no change. Added "negative" and "changed negative" fractions (:issue:`1690`, :pull:`1711`).
* ``make_criteria`` now skips columns with NaNs across all realizations. (:pull:`1713`).
* Fixed bug QuantileDeltaMapping adjustment not working for seasonal grouping (:issue:`1704`, :pull:`1716`).

Internal changes
^^^^^^^^^^^^^^^^
Expand Down
30 changes: 30 additions & 0 deletions tests/test_sdba/test_adjustment.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,36 @@ def test_mon_U(
# Test predict
np.testing.assert_allclose(p, ref.transpose(*p.dims), rtol=0.1, atol=0.2)

def test_seasonal(self, series, random):
u = random.random(10000)
kind = "+"
name = "tas"
# Define distributions
xd = uniform(loc=1, scale=1)
yd = uniform(loc=2, scale=4)

# Generate random numbers with u so we get exact results for comparison
x = xd.ppf(u)
y = yd.ppf(u)

# Test train
hist = sim = series(x, name)
ref = series(y, name)

QDM = QuantileDeltaMapping.train(
ref.astype("float32"),
hist.astype("float32"),
kind=kind,
group="time.season",
nquantiles=10,
)
p = QDM.adjust(sim.astype("float32"), interp="linear")

# Test predict
# Accept discrepancies near extremes
middle = (u > 1e-2) * (u < 0.99)
np.testing.assert_array_almost_equal(p[middle], ref[middle], 1)

def test_cannon_and_diagnostics(self, cannon_2015_dist, cannon_2015_rvs):
ref, hist, sim = cannon_2015_rvs(15000, random=False)

Expand Down
2 changes: 2 additions & 0 deletions xclim/sdba/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ def get_index(
ind = da.indexes[self.dim]
if self.prop == "week":
i = da[self.dim].copy(data=ind.isocalendar().week).astype(int)
elif self.prop == "season":
i = da[self.dim].copy(data=ind.month % 12 // 3)
else:
i = getattr(ind, self.prop)

Expand Down
12 changes: 11 additions & 1 deletion xclim/sdba/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ def _interp_on_quantiles_2D(newx, newg, oldx, oldy, oldg, method, extrap): # no
return out


SEASON_MAP = {"DJF": 0, "MAM": 1, "JJA": 2, "SON": 3}

map_season_to_int = np.vectorize(SEASON_MAP.get)


@parse_group
def interp_on_quantiles(
newx: xr.DataArray,
Expand Down Expand Up @@ -429,12 +434,17 @@ def interp_on_quantiles(
)
return out

# else:
if prop not in xq.dims:
xq = xq.expand_dims({prop: group.get_coordinate()})
if prop not in yq.dims:
yq = yq.expand_dims({prop: group.get_coordinate()})

# Adding the cyclic bounds fails for string coordinates like seasons
# That's why we map the seasons to integers
if prop == "season":
xq = xq.assign_coords(season=map_season_to_int(xq.season))
yq = yq.assign_coords(season=map_season_to_int(yq.season))

xq = add_cyclic_bounds(xq, prop, cyclic_coords=False)
yq = add_cyclic_bounds(yq, prop, cyclic_coords=False)
newg = group.get_index(newx, interp=method != "nearest")
Expand Down

0 comments on commit 93539ba

Please sign in to comment.