Skip to content
← Back to Workflows
workflowsBeginner· 13 min read

How to Download Rainfall Data: CHIRPS Monthly Files

Download free monthly rainfall for the whole tropics and sub-tropics, read the value for your place, build a year of rainfall as a table, and learn what a 5 km rainfall cell can and cannot tell you.

This guide has 2 routes. Pick one, or read both. Each route is complete on its own.

Rainfall is one of the most useful inputs for vegetation, drought, flood and water work, and it is hard to get from a single weather station. CHIRPS, the Climate Hazards group InfraRed Precipitation with Station data, is a rainfall dataset on a regular grid that covers the land between 50 degrees south and 50 degrees north from 1981 to the present. It combines satellite estimates with measurements from weather stations. The cell is 0.05 degrees, about 5 km, and files come as daily, pentad, monthly and annual totals.

This guide downloads the monthly files, because they are small, easy to read and enough for most seasonal work. It pairs well with the NDVI guides: vegetation follows rainfall with a delay of a few weeks.

Where the files are

The Climate Hazards Center keeps the files in an open folder, with no account and no login. The monthly global files for version 2.0 are at:

https://data.chc.ucsb.edu/products/CHIRPS-2.0/global_monthly/tifs/

A file name looks like chirps-v2.0.2023.07.tif.gz. v2.0 is the version, 2023.07 is the year and month, and .tif.gz means a GeoTIFF compressed with gzip. Each compressed file is about 14 MB, and it unpacks to about 58 MB. It covers the whole of 50 S to 50 N, so you download the whole band even if you want one district. The Center also publishes a newer version, 3.0, in its own folder. Check the archive page for the current version, and write down which one you used.

Route A: download in the browser

  1. Open the folder address above in your browser. It lists every month.
  2. Click the file for your month, for example chirps-v2.0.2023.07.tif.gz. It downloads to your computer.
  3. Unzip it. On Windows, a tool such as 7-Zip opens .gz files. You get chirps-v2.0.2023.07.tif.
  4. Open the .tif in QGIS. It has one band in latitude and longitude (EPSG:4326). Zoom to your area and use the Identify tool to read the rainfall of a cell.

The values are millimetres of rain in the month. Cells without data, such as the ocean, hold -9999, and the file does not say so. QGIS will treat -9999 as a real value and stretch the colours around it. Open Layer Properties > Transparency, tick Additional no data value, and enter -9999.

Route B: Python

You need Python with requests, numpy, pandas and rasterio.

import gzip
import shutil
from pathlib import Path

import requests

# Step 1: where the files are, and a folder to keep them in
BASE = "https://data.chc.ucsb.edu/products/CHIRPS-2.0/global_monthly/tifs/"
folder = Path("chirps")
folder.mkdir(exist_ok=True)

def get_month(year, month):
    # Step 2: build the file name. The month has two digits.
    name = f"chirps-v2.0.{year}.{month:02d}.tif"
    target = folder / name
    if target.exists():
        return target                                      # already downloaded, so skip

    # Step 3: download the compressed file, then unpack it
    zipped = folder / (name + ".gz")
    with requests.get(BASE + name + ".gz", stream=True, timeout=120) as reply:
        reply.raise_for_status()
        with open(zipped, "wb") as out:
            for chunk in reply.iter_content(chunk_size=1 << 20):
                out.write(chunk)
    with gzip.open(zipped, "rb") as src, open(target, "wb") as dst:
        shutil.copyfileobj(src, dst)
    zipped.unlink()                                        # remove the .gz, keep the .tif
    return target

# Step 4: one month, to see how it works
july = get_month(2023, 7)
print(july.name, "|", round(july.stat().st_size / 1e6), "MB")
chirps-v2.0.2023.07.tif | 58 MB

The function checks if the file is already there, so you can run the script again without downloading twice. stream=True downloads in pieces and does not hold the whole file in memory.

Read the facts of the file

import numpy as np
import rasterio

# Step 1: open the file and print what it is
with rasterio.open(july) as src:
    print("size:", src.width, "x", src.height, "cells | cell size:", round(src.res[0], 3), "degrees | CRS: EPSG", src.crs.to_epsg())
    print("extent:", [round(v, 1) for v in src.bounds])
    print("data type:", src.dtypes[0], "| NoData declared in the file:", src.nodata)

    # Step 2: read the whole band
    raw = src.read(1)

# Step 3: the file does not declare a NoData value, so -9999 is still in the numbers
print("lowest raw value:", float(raw.min()))

# Step 4: hide the -9999 cells yourself, then look again
rain = np.ma.masked_equal(raw, -9999)
print("lowest:", round(float(rain.min()), 1), "mm | highest:", round(float(rain.max()), 1), "mm | share of cells with data:", round(float(1 - rain.mask.mean()), 2))
size: 7200 x 2000 cells | cell size: 0.05 degrees | CRS: EPSG 4326
extent: [-180.0, -50.0, 180.0, 50.0]
data type: float32 | NoData declared in the file: None
lowest raw value: -9999.0
lowest: 0.0 mm | highest: 2152.3 mm | share of cells with data: 0.27

The size, 7200 by 2000 cells, is 360 degrees of longitude and 100 degrees of latitude at 0.05 degrees. The values are millimetres per month. The file does not declare a NoData value, so the cells without data, such as the ocean, still hold -9999 when you read them. If you took a mean without hiding them, the answer would be far too low. Always mask -9999 yourself.

The rainfall of your place

# Step 1: the longitude and latitude of your place. Jagraon, Punjab.
lon, lat = 75.47, 30.79

# Step 2: find the cell that contains the point, and read its value
with rasterio.open(july) as src:
    row, col = src.index(lon, lat)
    value = float(src.read(1)[row, col])
    cell_west, cell_north = src.xy(row, col, offset="ul")
    print("cell row", row, "column", col)
    print("the cell covers", round(cell_west, 2), "to", round(cell_west + 0.05, 2), "degrees east,", round(cell_north - 0.05, 2), "to", round(cell_north, 2), "degrees north")
print("July 2023 rainfall in that cell:", round(value, 1), "mm")
cell row 384 column 5109
the cell covers 75.45 to 75.5 degrees east, 30.75 to 30.8 degrees north
July 2023 rainfall in that cell: 275.7 mm

The cell is about 5 km wide. The number is the estimate for the whole cell and not for the point you gave. In Punjab a cell covers a block of fields and a part of a town. Keep that in mind when you compare it with a rain gauge.

A year of monthly rainfall

import pandas as pd

# Step 1: download the twelve months of 2023. Each file is about 14 MB compressed, so this takes a few minutes the first time.
rows = []
for month in range(1, 13):
    path = get_month(2023, month)
    with rasterio.open(path) as src:
        row, col = src.index(lon, lat)
        mm = float(src.read(1)[row, col])
    rows.append({"month": pd.Timestamp(2023, month, 1).strftime("%b"), "rain_mm": round(mm, 1)})

# Step 2: put them in a table and add the total for the year
rain_table = pd.DataFrame(rows).set_index("month")
print(rain_table.T.to_string())
print("total for 2023:", round(rain_table["rain_mm"].sum()), "mm")

# Step 3: save the table, so that the downloads do not need to be repeated
rain_table.to_csv("jagraon_rain_2023.csv")
month     Jan   Feb   Mar   Apr   May   Jun    Jul    Aug    Sep   Oct   Nov  Dec
rain_mm  28.0  11.1  60.6  13.5  55.5  42.5  275.7  115.7  201.1  40.8  11.5  4.4
total for 2023: 860 mm

The table has the shape of the monsoon: little rain in the dry months, and about 69 percent of the year's rain, 592 of 860 mm, in July, August and September. A table like this one lines up with the monthly NDVI series of the same year, so you can see how vegetation responds to rain.

Another source: GPM IMERG

CHIRPS is not the only free rainfall grid. GPM IMERG, from NASA and JAXA, estimates rain from a group of satellites on a 0.1 degree grid, about 11 km, from 2000 onward, with files from every half hour up to a month. It is a good second opinion on the same place. The files are kept by NASA, and downloading them needs a free Earthdata account.

To get one, register on the NASA Earthdata Login site, urs.earthdata.nasa.gov, and confirm your email. Then open your profile, go to Applications > Authorized Apps, and approve the NASA GESDISC DATA ARCHIVE application, because the IMERG files live in that archive. Last, generate a token from your profile and save it as an environment variable called EARTHDATA_TOKEN, so that the code reads it and you never paste it into a script. A token expires, so generate a new one if a download stops working.

The monthly files are about 19 MB each. The code below uses version 7, V07B, which is the final run for 2023. It needs h5py besides the packages used above. The file holds a group called Grid, with precipitation in millimetres per hour, which is the mean rate over the month, so you multiply by the number of hours to get a total.

import calendar
import os
from pathlib import Path

import h5py
import numpy as np
import pandas as pd
import requests

# Step 1: the token you created on your Earthdata profile. It is read from an environment variable, so it never sits in the script.
HEADERS = {"Authorization": "Bearer " + os.environ["EARTHDATA_TOKEN"]}
BASE = "https://data.gesdisc.earthdata.nasa.gov/data/GPM_L3/GPM_3IMERGM.07/"
folder = Path("imerg")
folder.mkdir(exist_ok=True)

def get_imerg_month(year, month):
    # Step 2: the monthly file name has the year, the month twice, and the version
    name = f"3B-MO.MS.MRG.3IMERG.{year}{month:02d}01-S000000-E235959.{month:02d}.V07B.HDF5"
    target = folder / name
    if not target.exists():
        with requests.get(f"{BASE}{year}/{name}", headers=HEADERS, stream=True, timeout=300) as reply:
            reply.raise_for_status()
            with open(target, "wb") as out:
                for chunk in reply.iter_content(chunk_size=1 << 20):
                    out.write(chunk)
    return target

# Step 3: one month, to see how big it is
july = get_imerg_month(2023, 7)
print(july.name, "|", round(july.stat().st_size / 1e6, 1), "MB")
3B-MO.MS.MRG.3IMERG.20230701-S000000-E235959.07.V07B.HDF5 | 19.0 MB
# Step 1: open the file and list what is in it
with h5py.File(july) as f:
    grid = f["Grid"]
    print("variables:", [k for k in grid.keys() if k in ("precipitation", "randomError", "precipitationQualityIndex", "gaugeRelativeWeighting")])
    print("precipitation shape:", grid["precipitation"].shape, "(time, longitude, latitude)")
    lon, lat = grid["lon"][:], grid["lat"][:]
    print("cell size:", round(float(lon[1] - lon[0]), 2), "degrees | first cell centre:", round(float(lon[0]), 2), round(float(lat[0]), 2))
    print("units:", grid["precipitation"].attrs["units"].decode() if isinstance(grid["precipitation"].attrs["units"], bytes) else grid["precipitation"].attrs["units"])
variables: ['precipitation', 'randomError', 'gaugeRelativeWeighting', 'precipitationQualityIndex']
precipitation shape: (1, 3600, 1800) (time, longitude, latitude)
cell size: 0.1 degrees | first cell centre: -179.95 -89.95
units: mm/hr
def point_value(path, lon_pt, lat_pt):
    # Step 1: find the cell that holds the point. The cell centres are 0.1 degrees apart, so the nearest centre is the cell.
    with h5py.File(path) as f:
        grid = f["Grid"]
        lons, lats = grid["lon"][:], grid["lat"][:]
        i = int(np.abs(lons - lon_pt).argmin())
        j = int(np.abs(lats - lat_pt).argmin())
        rate = float(grid["precipitation"][0, i, j])            # millimetres per hour, the mean rate over the month
    return rate, float(lons[i]), float(lats[j])

# Step 2: July 2023 at Jagraon, the same point as in the CHIRPS table
rate, cell_lon, cell_lat = point_value(july, 75.47, 30.79)
hours = calendar.monthrange(2023, 7)[1] * 24
print("cell centre:", round(cell_lon, 2), "E,", round(cell_lat, 2), "N")
print("mean rate:", round(rate, 3), "mm per hour over", hours, "hours")
print("July 2023 rainfall:", round(rate * hours, 1), "mm")
cell centre: 75.45 E, 30.75 N
mean rate: 0.337 mm per hour over 744 hours
July 2023 rainfall: 250.7 mm
# Step 1: all twelve months of 2023
rows = []
for month in range(1, 13):
    path = get_imerg_month(2023, month)
    rate, _, _ = point_value(path, 75.47, 30.79)
    rows.append({"month": pd.Timestamp(2023, month, 1).strftime("%b"), "imerg_mm": round(rate * calendar.monthrange(2023, month)[1] * 24, 1)})
imerg = pd.DataFrame(rows).set_index("month")

# Step 2: the CHIRPS values for the same cell, from the table earlier in this guide
imerg["chirps_mm"] = [28.0, 11.1, 60.6, 13.5, 55.5, 42.5, 275.7, 115.7, 201.1, 40.8, 11.5, 4.4]
print(imerg.T.to_string())
print("total for 2023: IMERG", round(imerg["imerg_mm"].sum()), "mm | CHIRPS", round(imerg["chirps_mm"].sum()), "mm")
print("correlation of the twelve months:", round(float(imerg["imerg_mm"].corr(imerg["chirps_mm"])), 3))
month       Jan   Feb   Mar   Apr   May    Jun    Jul    Aug    Sep   Oct   Nov  Dec
imerg_mm   16.4   4.0  83.3  49.0  75.1  122.4  250.7   57.3   97.2  23.1  14.4  0.0
chirps_mm  28.0  11.1  60.6  13.5  55.5   42.5  275.7  115.7  201.1  40.8  11.5  4.4
total for 2023: IMERG 793 mm | CHIRPS 860 mm
correlation of the twelve months: 0.84

The two sources agree on the shape of the year and disagree on the amounts. Both put the peak in July, 250.7 mm for IMERG and 275.7 mm for CHIRPS, and the twelve monthly values have a correlation of 0.84. The totals are 793 and 860 mm. The months differ a lot: June is 122 mm in IMERG and 43 mm in CHIRPS, and August and September are about half as much in IMERG, 57 against 116 mm and 97 against 201 mm. Two things explain part of this. The IMERG cell is twice as wide, so its cell centred at 75.45 E and 30.75 N covers 75.40 to 75.50 E and 30.70 to 30.80 N, an area four times that of the CHIRPS cell, and the two do not hold the same ground. And the two products combine satellites and gauges in different ways. A difference of this size is normal, and it is the reason to report which source you used. Which is closer to the truth at one place, only a rain gauge can say.

If you use more than one source in a study, keep each in its own column and say which is which.

What a CHIRPS cell can and cannot tell you

  • It is an estimate, built from satellite data and stations. In a region with few stations, it can miss local storms.
  • A 5 km cell is an average. Two fields in the same cell can get very different rain from the same storm.
  • Monthly totals hide how the rain fell. A month with 100 mm in one storm and a month with 100 mm in twenty light showers are the same number.
  • Compare with a rain gauge or an official source before you rely on a value for one place, and say which version of CHIRPS you used and when you downloaded it.

Common mistakes

  • Reading the values as metres or as millimetres per day. Monthly files are millimetres per month.
  • Taking the mean of the whole raster without hiding the -9999 cells. The file does not declare them, so nothing is hidden for you.
  • Downloading twelve global files for a single point without keeping them, and downloading them again next week. Keep the files, or save the table.
  • Mixing CHIRPS versions in one series.
  • Using the file name for the wrong year. The month part of the name has two digits.

Key takeaways