Skip to main content

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.

ImportantCHIRPS is not global

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.

TipThis is the low-latency option

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.

NoteThe scripts on this page

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"
done
for /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)}},
)
TipAlternative: CDO
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.nc

Faster, 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
done
for /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

WarningThe GeoTIFFs use -9999 and do not declare it

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.

TipAlternative: GDAL
gdalbuildvrt -separate chirps.vrt chirps-v3.0.*.tif
gdal_translate -of netCDF -a_nodata -9999 chirps.vrt chirps.nc

Quick, 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.

NoteOne CHIRPS-specific thing to carry forward

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

Back to top