Description
Currently, the ISIMIP v3.0 non-parametric quantile mapping helper _isimip_quantile_map_x_on_y_non_parametically (located in ibicus/utils/_math_utils.py) is vulnerable to missing values (NaN). A single NaN in a grid point's time-series window causes the entire window to be filled with NaNs in the debiased output.
The Issues
- Default
nan_policy in rankdata: Without specifying nan_policy in scipy.stats.rankdata(x), it defaults to propagating NaNs. If x has a single NaN, all ranks become NaN, resulting in p_x and the output z being entirely NaNs.
- Denominator Size: Using
x.size as the denominator when x contains NaNs shifts the empirical probabilities down.
- Reference array
y NaNs: If the reference/observations array y contains NaNs, np.sort(y) places them at the end. Since p_y = np.linspace(0.0, 1.0, y.size) is mapped to the sorted array, np.interp will return NaN for any high value in x (specifically whenever p_x > valid_fraction), causing artificial loss of valid data in the upper tail of the distribution.
Proposed Solution
Modify _isimip_quantile_map_x_on_y_non_parametically to:
- Ignore
NaNs when calculating ranks in x using nan_policy='omit'.
- Use the count of non-NaN elements in
x (valid_count) as the denominator.
- Filter out
NaNs from the reference array y before sorting and mapping (y_valid = y[~np.isnan(y)]). This prevents np.interp from propagating NaNs from the upper tail of y into valid outputs of z.
- Include a safety check for windows where all values are
NaN (valid_count == 0 or y_valid.size == 0) to prevent division by zero or errors in np.interp.
Proposed Code Changes
def _isimip_quantile_map_x_on_y_non_parametically(
x: np.ndarray, y: np.ndarray
) -> np.ndarray:
# 1. Filter out NaNs from reference observations to prevent interpolating against NaNs
y_valid = y[~np.isnan(y)]
# 2. Compute ranks omitting NaNs, keeping NaNs in their original positions
ranks = scipy.stats.rankdata(x, nan_policy='omit')
# 3. Denominator should be the count of valid elements in x, not x.size
valid_count = np.count_nonzero(~np.isnan(x))
# 4. Guard check for empty/all-NaN inputs to avoid division by zero or empty interpolation
if valid_count == 0 or y_valid.size == 0:
return np.full_like(x, np.nan, dtype=y.dtype)
# 5. Compute empirical ranks probability
p_x = (ranks - 1.0) / valid_count
p_y = np.linspace(0.0, 1.0, y_valid.size, dtype=y.dtype)
z = np.interp(p_x, p_y, np.sort(y_valid)).astype(y.dtype)
return z
Description
Currently, the ISIMIP v3.0 non-parametric quantile mapping helper
_isimip_quantile_map_x_on_y_non_parametically(located inibicus/utils/_math_utils.py) is vulnerable to missing values (NaN). A singleNaNin a grid point's time-series window causes the entire window to be filled withNaNs in the debiased output.The Issues
nan_policyinrankdata: Without specifyingnan_policyinscipy.stats.rankdata(x), it defaults to propagatingNaNs. Ifxhas a singleNaN, all ranks becomeNaN, resulting inp_xand the outputzbeing entirelyNaNs.x.sizeas the denominator whenxcontainsNaNs shifts the empirical probabilities down.yNaNs: If the reference/observations arrayycontainsNaNs,np.sort(y)places them at the end. Sincep_y = np.linspace(0.0, 1.0, y.size)is mapped to the sorted array,np.interpwill returnNaNfor any high value inx(specifically wheneverp_x > valid_fraction), causing artificial loss of valid data in the upper tail of the distribution.Proposed Solution
Modify
_isimip_quantile_map_x_on_y_non_parameticallyto:NaNs when calculating ranks inxusingnan_policy='omit'.x(valid_count) as the denominator.NaNs from the reference arrayybefore sorting and mapping (y_valid = y[~np.isnan(y)]). This preventsnp.interpfrom propagatingNaNs from the upper tail ofyinto valid outputs ofz.NaN(valid_count == 0ory_valid.size == 0) to prevent division by zero or errors innp.interp.Proposed Code Changes