How much rainfall has my region had this year compared to the 30-year normal?
Draw a rectangle, then check which collections cover it. Most NASA data is global, so a big box matches a lot.
74, 18 → 78, 21 (Maharashtra agricultural belt)Rainfall is the depth of water that fell from the sky, measured in millimetres. Satellites estimate it from orbit by watching clouds and storms, so you can add up how much rain your region got this year even with no weather stations on the ground. The trick is comparing that total to what's normal: the average for the same months across many past years. "60% of normal" means a dry year, "140%" a wet one.
How much rainfall has my region had this year vs the 30-year normal?
Rainfall is the depth of water that fell from the sky, measured in millimetres. Satellites estimate it from orbit by watching clouds and storms, so you can add up how much rain your region got this year even with no weather stations on the ground. The trick is comparing that total to what’s normal: the average for the same months across many past years. “60% of normal” means a dry year, “140%” a wet one.
Which data to use, and how
Use this dataset: GPM IMERG (monthly) (short name GPM_3IMERGM),
which gives one rainfall total per month per ~10 km pixel, covering 2000 → today, almost
everywhere on Earth. Monthly totals are the easiest place to start. For finer detail, like
day-by-day rainfall for the current year, switch to the daily version,
GPM IMERG (daily) (GPM_3IMERGDF).
Then do four steps. The runnable code further down does all of this; this is just the idea.
- Pick your region (a small latitude/longitude box around it) and the months you care about.
- Add up this year’s rain. Sum the rainfall inside your box over those months, giving one total (the “current year” number).
- Work out the normal. Do the same sum for every past year (back to 2000) and average them. That average is your baseline, the “normal” you compare against. Always use the same months in both, or the comparison is meaningless.
- Compare them. Express this year as a percent of normal. Above 100% = wetter than usual, below 100% = drier. Make a map so you can see which sub-areas are driest or wettest.
That’s the whole method. Below, “How a scientist answers this” names the exact tools, and the Run it cell does it live on synthetic data so you can see each step work.
What you can find out
- This year’s total rainfall for any region you draw.
- How this year compares to the normal: the percent-of-normal, using the 2000–2024 IMERG baseline.
- A dry-or-wet score: the anomaly index expressed as % of normal, which flags drought or flooding years.
- Where it’s driest or wettest. The map shows the pattern across sub-regions, not just one number for the whole area.
- Season by season. Split monsoon from non-monsoon, or wet season from dry, instead of lumping the whole year together.
What it can’t tell you
- A true 30-year normal. The satellite record (IMERG) only starts in 2000, so you have ~25 years, not 30. For a proper 30-year baseline you need an older or longer product: the earlier satellite era (TRMM, 1997–2015, tropics only) or a model reconstruction (MERRA-2, from 1980).
- Snowfall as water. Snow depth converted to its water content needs ground stations or dedicated snow data (SnowEx); the rainfall product alone won’t give it.
- Single-hour extreme storms outside the half-hourly grid. For sub-hourly bursts you need the half-hourly IMERG version, not the daily or monthly totals.
Gotchas to watch for
- The most accurate data arrives late. The best, fully-corrected version (IMERG “Final”) shows up about 3.5 months after the fact. If you need rainfall for right now, use the quicker versions (“Late” at ~12-hour delay, or “Early” at ~4-hour delay) and accept they’re a bit less accurate.
- Use the current version, not the old one. Version V07 is current; V06 is retired. If you cached any data from before 2023, re-download it so you’re not mixing versions.
- Mountains read low. In steep terrain (the Himalayas, the Western Ghats) the satellite systematically under-counts rain compared to ground gauges (an effect called orographic underestimation, where rain on mountainsides is missed). Cross-check against gauges if your region is mountainous.
- Always state your baseline. “140% of normal” is meaningless unless you say which years defined “normal.” A fixed window (e.g. 2000–2020) keeps different years comparable, so write it down every time.
The real-data code
The Run it cell above runs the method on synthetic data with no login. Below is the whole recipe against the real archive: search, open, accumulate this year, build the baseline normal, compute percent-of-normal and a standardized anomaly, and map it. It needs a free Earthdata Login.
import earthaccess, numpy as np, xarray as xr
import matplotlib.pyplot as plt
earthaccess.login(strategy="netrc")
# AOI: Maharashtra agricultural belt — a small (West, South, East, North) box.
aoi = (74, 18, 78, 21)
this_year = 2025
base_years = range(2000, 2021) # fixed baseline ("normal") window — state it!
# Open all monthly IMERG granules for a year, clip to the AOI, and return
# precipitation (mm/day per pixel) as one xarray DataArray (time, lat, lon).
def imerg_year(y):
grans = earthaccess.search_data(
short_name="GPM_3IMERGM", bounding_box=aoi,
temporal=(f"{y}-01-01", f"{y}-12-31"))
ds = xr.open_mfdataset(earthaccess.open(grans), combine="by_coords")
pr = ds["precipitation"].sel(
lon=slice(aoi[0], aoi[2]), lat=slice(aoi[1], aoi[3]))
return pr.load()
# 1. This year's rain: months are mm/day, so scale each by its day-count and
# sum over time -> annual total (mm) per pixel.
cur = imerg_year(this_year)
days = cur["time"].dt.days_in_month
cur_total = (cur * days).sum("time")
# 2. The normal: do the same per baseline year, then average across years.
yearly = []
for y in base_years:
py = imerg_year(y)
yearly.append((py * py["time"].dt.days_in_month).sum("time"))
stack = xr.concat(yearly, dim="year")
clim_mean = stack.mean("year")
clim_std = stack.std("year")
# 3. Percent-of-normal + standardized anomaly (z-score, the SPI-style drought number).
pct = cur_total / clim_mean * 100.0
z = (cur_total - clim_mean) / clim_std
# Area-mean the AOI (latitude weighting is negligible at this small box) for the headline.
w = np.cos(np.deg2rad(cur_total["lat"]))
pct_area = float(pct.weighted(w).mean(("lat", "lon")))
z_area = float(z.weighted(w).mean(("lat", "lon")))
label = "wetter" if pct_area > 100 else "drier"
print(f"{this_year}: {pct_area:.0f}% of the {base_years.start}-{base_years.stop-1} "
f"normal ({label} than usual), standardized anomaly z={z_area:+.2f}")
# 4. Map the percent-of-normal field: red = dry, white ~ normal, blue = wet.
pct.plot(cmap="BrBG", vmin=40, vmax=160, cbar_kwargs={"label": "% of normal"})
plt.title(f"{this_year} rainfall vs {base_years.start}-{base_years.stop-1} normal")
plt.tight_layout(); plt.show()
# Before you trust it: cross-check IMERG against CHIRPS or a rain gauge — satellite
# precip under-counts in steep terrain (the Western Ghats here; see the gotchas).
Expected output: a map of this year’s rainfall (mm) per pixel; a percent-of-normal map (red below 80%, green 80–120%, blue above 120%); a running total of this year’s rain against the normal range; and monsoon-season bars (this year’s June–Sept total vs the multi-year June–Sept mean).
Where the data comes from
Everything here is GPM IMERG, which lives in a single NASA archive (GES DISC) behind one free login (Earthdata Login). One sign-in covers both the daily and monthly versions.
Sources
- GPM IMERG: https://gpm.nasa.gov/data/imerg
- IMERG V07 user guide: https://gpm.nasa.gov/sites/default/files/2023-07/IMERG_V07_ReleaseNotes_230713-signed.pdf
Make it yours → Choose your region, the months, and the baseline; the notebook computes percent-of-normal and the drought index. Switch between IMERG and CHIRPS depending on whether you need global/recent or land-focused/long-record.
A safe place to practise the method on GPM_3IMERGM. The code is the real earthaccess workflow, but we feed it stand-in data shaped like the real product — so you can run it, change the numbers, and break it without an account. When you want the real data, use the recipe under “The real-data code” above, or download the notebook below.
Datasets used
Computed · real data Worked examples, computed end-to-end on real satellite data — not yet scientist-verified · expand ▾
We ran the analysis on real data for Indian regions and report exactly what came back — including the honest nulls and refusals. Each card shows the slope, significance, and (for rainfall) a gauge cross-check.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
The data doesn't support a rainfall change here — an earned null, not a missing answer.
…and what it refuses to answer
trend span < 10y — the validator refuses ill-posed questions rather than guessing. That discipline is the point.