Preparing CHIRPS
Monthly precipitation v3.0, from either the NetCDF or the GeoTIFF distribution
What CHIRPS is
CHIRPS is a quasi-global precipitation dataset from the Climate Hazards Center at UC Santa Barbara, blending satellite estimates with station observations. It runs from 1981 to the present at 0.05 degrees, and for SPI it is the usual choice where rainfall is the only variable you need.
The grid covers 60S to 60N, not pole to pole. Measured from the v3.0 monthly GeoTIFF: 2400 rows by 7200 columns, bounds lat -60.0 .. 60.0, lon -180.0 .. 180.0.
Anything above 60N or below 60S is outside the product, not missing from it. Plan your analysis domain accordingly, and do not compare a CHIRPS “global mean” with one from a genuinely global dataset such as TerraClimate.
| Version | v3.0 |
| Spatial resolution | 0.05 degrees, ~5 km |
| Grid | 2400 x 7200 = 17.3 million cells |
| Coverage | 60S-60N, all longitudes |
| Temporal resolution | Monthly (daily and pentad also published) |
| Record | 1981 to present |
| Latency | ~10-13 days after the month ends |
| Units | mm/month |
| Source | data.chc.ucsb.edu/products/CHIRPS/v3.0 |
For SPI you need one variable, precipitation, so there is no PET step and no water balance. That makes CHIRPS considerably less work than TerraClimate.
CHIRPS publishes monthly, roughly a fortnight after the month ends. Measured publication dates for the last four complete months, checked 2026-09-29:
| Month | Published | Days after month end |
|---|---|---|
| May 2026 | 12 June 2026 | 11 |
| June 2026 | 14 July 2026 | 13 |
| July 2026 | 14 August 2026 | 13 |
| August 2026 | 11 September 2026 | 10 |
The by_year NetCDF is rewritten as each month lands, so chirps-v3.0.2026.monthly.nc held 8 months when checked, at 131.3 MB against ~195 MB for a complete year.
For comparison, TerraClimate releases one file per calendar year and the current year is absent entirely: on the same date its newest data was 2025. If you are monitoring an ongoing drought rather than studying the record, that difference decides it. See Preparing TerraClimate.
There is no preliminary monthly CHIRPS at this path: a request for the current month returns 404 until the final product is published.
Two distributions, same data
CHC publishes monthly CHIRPS in two forms. Both give the same numbers; they differ in how much you download and how much preparation each needs.
| NetCDF | GeoTIFF | |
|---|---|---|
| Layout | One file for everything, or one per year | One file per month |
| Size | 8.9 GB whole, or ~197 MB per year | 23.1 MB per month |
| Full record 1981-2025 | ~8.9 GB | ~12.5 GB (540 files) |
| Coordinates | latitude / longitude |
n/a, needs building |
| Missing data | Declared via _FillValue |
Not declared. Uses -9999 |
| Packed integers | No | No |
| Work to prepare | Rename coordinates | Stack, build a time axis, mask -9999 |
Use the NetCDF unless you have a reason not to. The GeoTIFF route exists because some pipelines already work in tiles, or because you want a handful of months rather than the whole record.
Helpers live in docs/tutorials/scripts/. Imports below assume you are working from docs/tutorials/; adjust the path or copy the modules next to your code.
Route A: NetCDF
Download
Preferred. Resumable, restartable, and you can start work before the whole record has arrived.
for y in $(seq 1981 2025); do
wget -c "https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/global/netcdf/by_year/chirps-v3.0.${y}.monthly.nc"
donefor /L %y in (1981, 1, 2025) do wget -c ^
"https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/global/netcdf/by_year/chirps-v3.0.%y.monthly.nc"One 8.9 GB download covering the whole record. Simple, but a dropped connection at 80% costs you everything unless your client resumes.
wget -c "https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/global/netcdf/chirps-v3.0.monthly.nc"from pathlib import Path
from scripts.tc_prepare import download_year # generic, not TerraClimate-only
BASE = ("https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/"
"global/netcdf/by_year")
out = Path("raw")
for year in range(1981, 2026):
download_year("chirps-v3.0", year, out, base=BASE)download_year builds {base}/{var}_{year}.nc, so for CHIRPS pass the stem and rename, or call it as a template. For a one-off, wget -c is less fuss.
Prepare
CHIRPS NetCDF is already unpacked, already in mm/month, and already on a regular monthly axis. Two things need attention.
The coordinates are latitude and longitude, not lat and lon. The package auto-detects either, but standardising once keeps everything downstream simple.
Merging the yearly files needs the time axis sorted and checked.
from pathlib import Path
import xarray as xr
from scripts.nc_fixups import standardise_names, clean_missing, write_cf_metadata
files = sorted(Path("raw").glob("chirps-v3.0.*.monthly.nc"))
ds = xr.open_mfdataset(files, combine="by_coords", engine="netcdf4")
ds = standardise_names(ds) # latitude -> lat, longitude -> lon, CF attrs
ds = clean_missing(ds, "precip") # -9999 and friends -> NaN
ds = write_cf_metadata(
ds,
title="CHIRPS v3.0 monthly precipitation",
institution="Climate Hazards Center, UC Santa Barbara",
source="CHIRPS v3.0",
references="Funk et al. (2015), Scientific Data 2, 150066",
)
ds["precip"] = ds["precip"].astype("float32")
ds.to_netcdf(
"wld_cli_chirps3_precip_1981_2025.nc",
encoding={"precip": {"dtype": "float32", "_FillValue": -9999.0,
"zlib": True, "complevel": 5,
"chunksizes": (ds.sizes["time"], 128, 128)}},
)cdo -z zip_5 mergetime chirps-v3.0.*.monthly.nc wld_cli_chirps3_precip_1981_2025.nc
ncrename -d latitude,lat -d longitude,lon -v latitude,lat -v longitude,lon out.ncFaster, and fine here because CHIRPS needs no unpacking. You give up control of the output chunking, which matters if you will read per-pixel time series: see Preparing Your Data on chunking for how you read.
Route B: GeoTIFF
One file per month, named chirps-v3.0.{yyyy}.{mm}.tif with a zero-padded month.
Download
for y in $(seq 1981 2025); do
for m in 01 02 03 04 05 06 07 08 09 10 11 12; do
wget -c "https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/global/tifs/chirps-v3.0.${y}.${m}.tif"
done
donefor /L %y in (1981, 1, 2025) do for %m in (01 02 03 04 05 06 07 08 09 10 11 12) do ^
wget -c "https://data.chc.ucsb.edu/products/CHIRPS/v3.0/monthly/global/tifs/chirps-v3.0.%y.%m.tif"540 files, about 12.5 GB.
The trap in this route
Read straight from the remote file:
declared nodata : None
dtype : float32
location lon lat value
mid Pacific ocean -140.00 0.00 -9999.0000
south Atlantic ocean -25.00 -40.00 -9999.0000
Amazon, land -60.00 -3.00 347.3873
Sahara, land 10.00 23.00 0.3212
above the band (Arctic) 0.00 59.50 -9999.0000
Because no nodata tag is set, every reader treats -9999 as a measurement. Nothing errors. You get an ocean of negative rainfall, a global mean that is meaningless, and if it reaches a distribution fit, a result that is confidently wrong.
So pass the sentinel explicitly:
tiffs_to_netcdf(..., nodata=-9999.0)The second trap is the half-pixel shift, covered in Preparing Your Data. CHIRPS makes it concrete: the geotransform origin is exactly -180.0, 60.0, which is the corner of the first pixel. Its centre is -179.975, 59.975. Copy the origin into a coordinate array and every cell is 2.5 km out of position.
Convert
from scripts.tiff2nc import tiffs_to_netcdf
tiffs_to_netcdf(
"tifs/",
"wld_cli_chirps3_precip_1981_2025.nc",
var_name="precip",
glob="chirps-v3.0.*.tif",
date_pattern=r"chirps-v3\.0\.(\d{4}\.\d{2})\.tif",
date_format="%Y.%m",
units="mm",
standard_name="precipitation_amount",
long_name="monthly precipitation",
nodata=-9999.0, # not declared in the file
lat_order="descending", # keep the GeoTIFF row order
)The date pattern captures 1981.01 and %Y.%m parses it, so the files sort by real date rather than by filename. tiffs_to_netcdf also verifies every raster shares the first file’s grid, which matters across a 540-file download where one partial file would otherwise be stacked in silently.
gdalbuildvrt -separate chirps.vrt chirps-v3.0.*.tif
gdal_translate -of netCDF -a_nodata -9999 chirps.vrt chirps.ncQuick, and it gets the cell centres right. What you do not get is a time coordinate: the bands come out as band1..band540 with no dates attached, so you still have to build the time axis yourself before any index code will read it.
Check before you compute
import numpy as np
import xarray as xr
ds = xr.open_dataset("wld_cli_chirps3_precip_1981_2025.nc")
p = ds["precip"]
print(f"dims : {p.dims} {p.shape}")
print(f"time : {str(p.time.values[0])[:10]} to {str(p.time.values[-1])[:10]}")
print(f"units : {p.attrs.get('units')}")
print(f"lat : {float(p.lat.min()):.3f} .. {float(p.lat.max()):.3f}")
last = p.isel(time=-1).values
print(f"finite : {np.isfinite(last).sum():,} of {last.size:,}")
print(f"range : {np.nanmin(last):.2f} .. {np.nanmax(last):.2f} mm")
# The check that catches the GeoTIFF trap.
assert np.nanmin(last) >= 0, "negative rainfall: the -9999 sentinel got through"
# And the one that catches a truncated stack.
expected = (2025 - 1981 + 1) * 12
assert p.sizes["time"] == expected, f"{p.sizes['time']} steps, expected {expected}"A correct CHIRPS v3.0 monthly series over 1981-2025 has 540 time steps on a 2400 x 7200 grid, with latitude spanning -59.975 to 59.975.
What you should have
wld_cli_chirps3_precip_1981_2025.nc precip, float32, (540, 2400, 7200)
Unpacked (CHIRPS never was packed), in mm/month, NaN over ocean and outside the 60S-60N band, coordinates named lat and lon, and 540 monthly steps.
That is the input contract for spi(). CHIRPS needs no PET and no water balance, so there is nothing further to prepare.
CHIRPS has many exact zeros, in arid regions and in the dry season everywhere. Gamma is undefined at zero. This package handles that for you by fitting a mixed distribution and carrying the zero probability separately, so do not pre-substitute a small value such as 0.001 - see Preparing Your Data.
Next step
Your data is ready. The calculation itself, including calibration periods, multi-scale SPI, distribution choice and saving fitting parameters for operational reuse, is Calculate SPI.
For the whole global grid rather than a region, the tiled and concurrent routes are described in Implementation.
To clip to a country or basin first, see Preparing Your Data. The boundary rule matters more than it looks.
See Also
- Preparing Your Data - the general input contract
- Preparing TerraClimate - for SPEI, which also needs PET
- Data Model & Outputs - full input specification
- Calculate SPI - using these files