xclim icon indicating copy to clipboard operation
xclim copied to clipboard

Add flexibility to handling probability of zero-precipitation events in Standardized Indices (SPI)

Open Hem-W opened this issue 5 months ago • 0 comments

Addressing a Problem?

The calculation of the Standardized Precipitation Index (SPI) involves transforming a fitted probability distribution (often Gamma) into a standard normal distribution. A critical step in this process is handling "zero-inflated" data (months with no precipitation).

Different methodologies exist for assigning a probability to these zero events before the normal transformation:

  • Traditional/McKee Method ("Upper"): Assigns the full cumulative probability of zero, $P(x=0) = p_0$. This is known to introduce a positive bias in the mean SPI, especially in arid regions where $p_0$ is large, as it effectively treats all zero events as "wetter" than they should be.

  • Center of Mass Method ("Center"): Assigns the probability $p_0 / 2$. This is recommended by Stagge et al. (2015) to correct the bias and ensure the SPI distribution remains centered at 0.

Reference: Stagge et al. (2015) Candidate Distributions for Climatological Drought Indices (SPI and SPEI)

Potential Solution

Adding a prob_zero_method parameter to standardized_index and standardized_precipitation_index, which allow users to specify whether use "upper", "center" or customized callable as the probability corresponding to zero-precipitation.

Additional context

# %%
import xarray as xr
import numpy as np
import scipy.stats
from xclim.indices import standardized_precipitation_index
from xclim.indices.stats import standardized_index_fit_params

# Use cftime to create a NOLEAP calendar (365 days every year)
# This ensures a strict 365-day periodicity.
time = xr.cftime_range("2000-01-01", "2010-12-31", calendar="noleap", freq="D")
pr = xr.DataArray(
    np.random.rand(len(time)) * 10,
    coords={"time": time},
    dims="time",
    name="pr",
    attrs={"units": "mm/day"}
)

# Force DOY 180 to be ZERO for ALL 11 years.
pr[{"time": slice(179, None, 365)}] = 0

# Parameters for fitting
fit_params = dict(
    freq=None,
    window=1,
    dist="gamma",
    method="ML",
    doy_bounds=(180, 180),
)

# 1. Get fitting parameters
params = standardized_index_fit_params(pr, zero_inflated=True, **fit_params)
prob_of_zero = params.prob_of_zero.sel(dayofyear=180).values
print(f"Probability of Zero (p0): {prob_of_zero:.4f}")

# 2. Compute SPI
# Original zero-precipitation probability: "upper" (Biased) -> P = p0
spi_upper = standardized_precipitation_index(
    pr, params=params, **fit_params
)

# Verification: Check the SPI value for the first occurrence (index 179)
t_idx = 179
val_upper = spi_upper.isel(time=t_idx).values

print(f"\nResults for Zero-Precipitation Day (p0=1.0):")
print(f"Upper (p0):      SPI={val_upper:.4f}")

This will leads to Probability of Zero (p0): 1.0000 and SPI=8.2100, which may leads to misleading "extreme wet".

Contribution

  • [x] I would be willing/able to open a Pull Request to contribute this feature.

Code of Conduct

  • [x] I agree to follow this project's Code of Conduct

Hem-W avatar Nov 24 '25 14:11 Hem-W