drought-monitoring
A Python package for computing the Composite Drought Index (CDI) over 20–30 year monitoring periods using Google Earth Engine, with in-memory dask-parallelised spatial computation, Cloud Optimized GeoTIFF export, and publication-quality climate science visualisations.
Installation
# pip
pip install drought-monitoring
# uv
uv add drought-monitoring
# with all optional dependencies (GEE + COG + plots)
pip install "drought-monitoring[all]"
Optional extras:
| Extra | Installs | Use for |
|---|---|---|
gee |
earthengine-api |
fetching GEE data |
cog |
rasterio, rioxarray |
COG export / import |
plot |
matplotlib |
visualisation |
all |
all of the above | full workflow |
Package structure
drought_monitoring/
├── core.py CDI mathematics on pd.Series
├── spatial.py pixel-wise computation on xr.DataArray (dask-parallelised)
├── gee.py GEE authentication + ERA5-Land / MODIS xee cube fetching
├── io.py Cloud Optimized GeoTIFF (COG) export and import
└── plot.py publication-quality climate science figures
tests/
└── test_core.py
pyproject.toml
README.md
Typical Jupyter notebook workflow
See workshop/drought_monitoring_TUK_workshop.ipynb for the full worked example (Turkana County, Kenya, 2000–2025).
1. Authenticate GEE
from drought_monitoring.gee import authenticate
authenticate(project="my-gee-project") # opens browser on first use
2. Define your Area of Interest
# Format: [lon_min, lat_min, lon_max, lat_max]
aoi = [34.5, 1.5, 36.5, 5.5] # Turkana County, Kenya
3. Fetch 25 years of monthly data
from drought_monitoring.gee import fetch_era5_precip, fetch_era5_temp, fetch_modis_ndvi
precip = fetch_era5_precip(aoi, start_year=2000, end_year=2025)
temp = fetch_era5_temp(aoi, start_year=2000, end_year=2025)
ndvi = fetch_modis_ndvi(aoi, start_year=2000, end_year=2025)
# Each returns a pd.Series with a monthly DatetimeIndex (312 months)
4. Compute the full CDI
from drought_monitoring import compute_all
df = compute_all(precip, temp, ndvi, window=3, weights=(0.50, 0.25, 0.25))
# pd.DataFrame with columns: PDI, TDI, VDI, CDI
5. Classify drought severity
from drought_monitoring.plot import classify_cdi
df["severity"] = classify_cdi(df["CDI"])
print(df["severity"].value_counts(normalize=True).mul(100).round(1))
6. Publication-quality plots
from drought_monitoring.plot import plot_timeseries, plot_anomaly_bars, plot_seasonal_cycle
import matplotlib.pyplot as plt
fig = plot_timeseries(
df,
title = "Composite Drought Index — Turkana County, Kenya",
subtitle = "ERA5-Land + MODIS MOD13A3 | 2000–2025",
show_components = True,
show_severity_bar = True,
)
fig.savefig("CDI_timeseries_Turkana.png", dpi=150, bbox_inches="tight")
fig2 = plot_anomaly_bars(df, column="CDI", freq="Y",
title="Annual Mean CDI Anomaly — Turkana County")
fig2.savefig("CDI_annual_Turkana.png", dpi=150, bbox_inches="tight")
fig3 = plot_seasonal_cycle(df, title="Seasonal Cycle — Turkana 2000–2025")
fig3.savefig("CDI_seasonal_Turkana.png", dpi=150, bbox_inches="tight")
7. Generate annual spatial CDI maps
from drought_monitoring.gee import yearly_drought_maps
# Streams ERA5 + MODIS via xee, computes CDI pixel-wise with dask,
# resamples to annual means — nothing written to disk
ds = yearly_drought_maps(aoi, start_year=2000, end_year=2025)
# xr.Dataset with variables: PDI, TDI, VDI, CDI | dims: (time, lat, lon)
ds["CDI"].plot(col="time", col_wrap=4, cmap="RdBu", robust=True, figsize=(18, 14))
8. Export to Cloud Optimized GeoTIFFs
import os
from drought_monitoring.io import cdi_stack_to_cog
os.makedirs("outputs", exist_ok=True)
paths = cdi_stack_to_cog(ds, output_dir="outputs/", prefix="Turkana_2000_2025")
# outputs/Turkana_2000_2025_PDI.tif (26 bands, one per year)
# outputs/Turkana_2000_2025_TDI.tif
# outputs/Turkana_2000_2025_VDI.tif
# outputs/Turkana_2000_2025_CDI.tif
9. Visualise COGs interactively
import leafmap
m = leafmap.Map(center=[3.5, 35.5], zoom=7)
m.add_cog_layer("outputs/Turkana_2000_2025_CDI.tif", name="CDI")
m
10. Run a 6-month drought forecast
from drought_monitoring.forecast import forecast_all_statistical
from drought_monitoring.plot import plot_forecast
fc = forecast_all_statistical(
precip, temp, ndvi,
n_months = 6,
weights = (0.50, 0.25, 0.25),
ci_level = 0.90,
)
# pd.DataFrame with columns: PDI, TDI, VDI, CDI, CDI_lower, CDI_upper, lead
fig_fc = plot_forecast(
history = df,
forecast = fc,
n_history = 36,
title = "Turkana CDI — 6-month Drought Outlook",
subtitle = "STL + SARIMA statistical forecast | 90% confidence band",
show_components = True,
)
fig_fc.savefig("CDI_forecast_Turkana.png", dpi=150, bbox_inches="tight")
Area-averaged time series workflow
For a single-pixel or spatially-averaged CDI time series:
from drought_monitoring.gee import fetch_era5_precip, fetch_era5_temp, fetch_modis_ndvi
from drought_monitoring import compute_all
from drought_monitoring.plot import plot_timeseries, plot_anomaly_bars
precip = fetch_era5_precip(aoi, start_year=2000, end_year=2020)
temp = fetch_era5_temp(aoi, start_year=2000, end_year=2020)
ndvi = fetch_modis_ndvi(aoi, start_year=2000, end_year=2020)
df = compute_all(precip, temp, ndvi, window=3)
# pd.DataFrame with columns: PDI, TDI, VDI, CDI
fig = plot_timeseries(df, title="CDI — Borena Region",
subtitle="ERA5-Land + MODIS MOD13A3 | 2000–2020")
fig.savefig("CDI_timeseries.png", dpi=300, bbox_inches="tight")
fig2 = plot_anomaly_bars(df, title="Annual Mean CDI Anomaly")
fig2.savefig("CDI_annual.png", dpi=300, bbox_inches="tight")
Data sources
| Variable | GEE collection | Band | Units |
|---|---|---|---|
| Precipitation | ECMWF/ERA5_LAND/MONTHLY_AGGR |
total_precipitation_sum |
mm month⁻¹ |
| Temperature | ECMWF/ERA5_LAND/MONTHLY_AGGR |
temperature_2m |
°C |
| NDVI | MODIS/061/MOD13A3 |
NDVI |
[−1, 1] |
CDI formula
Each sub-index follows Burchard-Levine et al. (2024):
DI = (μ_IP / μ_LTM) × sqrt(RL_IP / RL_LTM)
CDI = 0.50 × PDI + 0.25 × TDI + 0.25 × VDI
| Symbol | Meaning |
|---|---|
μ_IP |
Trailing rolling mean over the interest period (default 3 months) |
μ_LTM |
Long-term mean of μ_IP for that calendar month |
RL_IP |
Count of months inside the IP where the anomaly condition holds |
RL_LTM |
Long-term mean of RL_IP for that calendar month |
PDI & VDI use deficit streaks (raw < monthly LTM).
TDI uses excess streaks (raw > monthly LTM).
CDI severity classes
| CDI value | Class |
|---|---|
| < 0.50 | Extreme drought |
| 0.50 – 0.65 | Severe drought |
| 0.65 – 0.80 | Moderate drought |
| 0.80 – 0.90 | Mild drought |
| 0.90 – 1.10 | Near normal |
| 1.10 – 1.20 | Mild wet |
| 1.20 – 1.30 | Moderately wet |
| > 1.30 | Very wet |
Values < 1 indicate drought; values ≈ 1 are near-normal; values > 1 are wetter than normal.
Monitoring period
| Default | Minimum | Maximum |
|---|---|---|
| 5 years | 5 years | 30 years |
An error is raised if the requested period is outside this range.
COG structure
Each output GeoTIFF contains one band per timestep.
Band descriptions are ISO-8601 date strings (YYYY-MM or YYYY), so every
file is self-documenting and can be range-requested from cloud storage
(S3, GCS, Azure Blob) by leafmap, QGIS, or any GDAL tool.
Running tests
pytest tests/ -v
Reference
Based on: Burchard-Levine, V. et al. (2024). pyCDI: a Python implementation of the composite drought index. EO-Africa R&D, ESA. https://github.com/VicenteBurchard/pyCDI
Metadata
Release files for drought-monitoring 0.1.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| drought_monitoring-0.1.8.tar.gz | 36.2 kB | Without provenance | Details for drought_monitoring-0.1.8.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| drought_monitoring-0.1.8-py3-none-any.whl | Python 3 | none | any | Details for drought_monitoring-0.1.8-py3-none-any.whl |
Total release size: 70.7 kB
Release files / drought_monitoring-0.1.8.tar.gz
| Download URL | drought_monitoring-0.1.8.tar.gz |
|---|---|
| Size | 36.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
57c1003538ec51504a7076d8aafe51c4b7d41be87dd5f4e2cad12418f9a5c669
|
|
BLAKE2b-256 checksum How to use checksums |
1cc9836150f727afed4b7fac501e2ba5dc58d7305c46d1c0cc3b643fca29b51c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / drought_monitoring-0.1.8-py3-none-any.whl
| Download URL | drought_monitoring-0.1.8-py3-none-any.whl |
|---|---|
| Size | 34.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83b378beafd8626b2e8b484e2e9d9eda87d1e14950e1f20fa7fd1775d51be3f2
|
|
BLAKE2b-256 checksum How to use checksums |
1b108354f6cc992473274e6f02e50971bb16bca5358da10331f7f346a3b59c3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for drought-monitoring 0.1.8
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.