Regridding Explained: Bilinear, Conservative and Nearest for Gridded Data

Problem statement

Comparing a model with observations, feeding one dataset into another's grid, or averaging several products together all need the data on the same grid. Regridding does that, and the method decides what survives the move. Bilinear interpolation keeps values at points, conservative remapping keeps totals over areas, and nearest-neighbour keeps the original values and categories — and each one quietly breaks the other two.

Measured by regridding NOAA CPC's 0.5° global daily land precipitation for 2024 to three coarser grids — the 1.875° × 1.25° N96 grid used by the UK's UKESM1 model, 2.5° and 5°:

  • Conservative remapping kept every regional total exact — global land 110,380 km³ of rain, the Amazon, South Asia and the British Isles — but only when totals were multiplied by the area of land data in each target cell. Multiplied by full cell areas, the global total rose by 31.2% at N96.
  • Bilinear interpolation lost 11.7% of the global total at N96, and 15.2% over the British Isles, because it returns NaN wherever a coastal neighbour is missing.
  • Bilinear read only 42.7% of the source cells at N96, 16.0% at 2.5° and 4.0% at 5°. Everything else never influenced the result.
  • The wettest day's peak of 795.9 mm became 374.6 mm conservatively, 546.4 mm bilinearly and 534.4 mm by nearest neighbour at N96.

Quick answer

Choose the method from what must be preserved:

field or question                                 method
fluxes and totals: precipitation, emissions      conservative
smooth state fields at points: temperature       bilinear
categories: land cover, masks, basin IDs         nearest neighbour

On regular latitude–longitude grids, bilinear and nearest are built into xarray; conservative remapping needs overlap weights, provided by libraries such as xESMF or computed directly (Example 1):

import numpy as np
import xarray as xr

pr = xr.open_dataset("cpc_precip.2024.nc")["precip"].sortby("lat")   # 0.5°, land only
annual = pr.sum("time", min_count=1)                                  # mm in 2024

lat = np.arange(-89.375, 90, 1.25)                                    # the N96 grid
lon = np.arange(0.9375, 360, 1.875)
bilinear = annual.interp(lat=lat, lon=lon, method="linear")
nearest = annual.interp(lat=lat, lon=lon, method="nearest")
conservative, covered = conservative_regrid(annual, lat, lon)          # Example 1
Table of how each target cell is computed: bilinear from the four surrounding source centres, conservative from every overlapping source cell weighted by overlap area, and nearest from the single closest source cell.
Bilinear reads points, conservative reads areas, nearest reads one cell.

Step-by-step solution

1. Decide what the result must preserve

A precipitation grid is a flux: each cell's value times its area is a volume of water. Regridding it for a water budget must keep volumes. A temperature grid is a state: the aim is a good estimate at each target location. A land-cover or basin grid is categorical: an average of two class codes is meaningless. The method follows from that, not from what is quickest to type.

2. Know whether you are coarsening or refining

Moving from 0.5° to 2.5° puts 25 source cells in each target cell. Moving from 2.5° to 0.5° creates 25 target cells from each source cell. Coarsening is where methods differ most: conservative averages all 25, bilinear reads the four whose centres surround the target centre and ignores the rest.

3. Understand what bilinear interpolation reads

Bilinear interpolation evaluates the field at the target cell's centre from the four nearest source centres. When coarsening, most source cells are never touched: 42.7% were read for the N96 grid, 16.0% for 2.5° and 4.0% for 5°. A storm that falls between target centres simply disappears, and one that falls on a target centre is copied across a cell far larger than the storm. At 5° that sampling error happened to push the Amazon total up by 7.1% while the global total fell by 8.5%.

4. Understand what conservative remapping computes

First-order conservative remapping sets each target cell to the area-weighted mean of every source cell overlapping it, with the overlap measured on the sphere. The weights are the fractions of source area inside each target cell, so every source value contributes and the area integral is unchanged. It costs more than interpolation — 0.046 s against 0.095 s at N96 here, and 0.093 s against 0.007 s at 2.5° — but it is still fast on a regular grid.

5. Handle masked cells, and multiply by the covered area

CPC precipitation exists only over land. A coastal target cell that is 30% land gets the mean of its land source cells, which is right as a depth in millimetres. Multiplied by the target cell's full area, that depth counts the sea as if rain fell on it at the land rate: totals rose by 31.2% globally and 61.0% over the British Isles at N96, 45.2% and 81.9% at 2.5°. Keep the area of valid source data in each target cell alongside the regridded field and use it for totals. With it, every total matched the source to 0.0%.

6. Use nearest neighbour for categories

Nearest-neighbour regridding copies the value of the source cell whose centre is closest. It never invents values, so class codes stay valid, but when coarsening it discards the other cells just as bilinear does. For precipitation it overstated the British Isles total by 10.1% at N96 and understated it by 18.1% at 2.5°. For categorical data coarsened a long way, a majority rule over the overlapping cells is often better than the nearest one.

7. Expect extremes to change

A conservative cell value is an average over a larger area, so it is lower than the wettest point inside it. The 795.9 mm peak on 28 December 2024 became 374.6 mm at N96, 216.5 mm at 2.5° and 83.1 mm at 5°. Bilinear kept more of the peak at N96 (546.4 mm) because the target centre happened to fall near it — and lost almost all of it at 5° (34.7 mm) because it did not. A regridded extreme is a property of the grid as much as of the weather.

8. Check totals and coverage after every regrid

Compute the total of the source and the target with their own cell areas, region by region, and compare (Example 2). A few lines catch the missing-coast and full-cell-area errors immediately.

Bar chart of the change in global land precipitation total after regridding to the N96 grid by conservative remapping with covered area, conservative with full cell area, bilinear and nearest.
The same conservative field gave an exact total or a 31% overestimate depending only on the area it was multiplied by.

Code examples

Example 1 — first-order conservative regridding for regular grids

import numpy as np
import xarray as xr

EARTH_RADIUS_KM = 6371.0088


def edges(centres, lo, hi):
    """Cell edges from regularly spaced centres, clipped to the valid range."""
    c = np.asarray(centres, dtype="float64")
    e = np.concatenate([[c[0] - (c[1] - c[0]) / 2], (c[:-1] + c[1:]) / 2, [c[-1] + (c[-1] - c[-2]) / 2]])
    return np.clip(e, lo, hi)


def overlap(src_edges, dst_edges):
    lo = np.maximum(dst_edges[:-1, None], src_edges[None, :-1])
    hi = np.minimum(dst_edges[1:, None], src_edges[None, 1:])
    return np.clip(hi - lo, 0, None)


def conservative_regrid(da, lat, lon, x="lon", y="lat"):
    """Regrid between rectilinear 0-360 lat-lon grids, keeping area integrals.

    Returns the regridded field and the area (km2) of valid source data in each target cell.
    """
    da = da.sortby(y)
    lat, lon = np.sort(lat), np.sort(lon)
    wy = overlap(np.sin(np.deg2rad(edges(da[y], -90, 90))), np.sin(np.deg2rad(edges(lat, -90, 90))))
    wx = overlap(np.deg2rad(edges(da[x], 0, 360)), np.deg2rad(edges(lon, 0, 360)))
    field = da.transpose(..., y, x).values
    valid = ~np.isnan(field)
    covered = EARTH_RADIUS_KM**2 * (wy @ valid.astype("float64") @ wx.T)
    summed = EARTH_RADIUS_KM**2 * (wy @ np.where(valid, field, 0.0) @ wx.T)
    with np.errstate(invalid="ignore", divide="ignore"):
        out = np.where(covered > 0, summed / covered, np.nan)
    dims = [d for d in da.dims if d not in (x, y)] + [y, x]
    coords = {d: da[d] for d in dims[:-2]} | {y: lat, x: lon}
    return (xr.DataArray(out, dims=dims, coords=coords, attrs=da.attrs),
            xr.DataArray(covered, dims=dims, coords=coords, attrs={"units": "km2"}))

The latitude overlaps are measured in sin(latitude), which makes them proportional to area on the sphere; longitude overlaps are in radians. Both grids must use 0–360 longitude.

Example 2 — compare totals before and after

def cell_area_km2(lat, lon):
    lat_e, lon_e = edges(lat, -90, 90), edges(lon, 0, 360)
    return EARTH_RADIUS_KM**2 * np.outer(np.diff(np.sin(np.deg2rad(lat_e))), np.deg2rad(np.diff(lon_e)))


def total_km3(depth_mm, area_km2):
    return float(np.nansum(depth_mm * area_km2)) / 1e6


def compare_totals(source, lat, lon, results):
    """Total volume (km3) before and after, for each regridded version."""
    src = total_km3(source.sortby("lat").values, cell_area_km2(np.sort(source.lat), source.lon))
    full = cell_area_km2(lat, lon)
    print(f"{'source':30s} {src:10,.0f} km3")
    for name, (field, area) in results.items():
        tot = total_km3(field.values, full if area is None else area.values)
        print(f"{name:30s} {tot:10,.0f} km3  {(tot / src - 1) * 100:+6.1f}%")


compare_totals(annual, lat, lon, {
    "conservative x covered area": (conservative, covered),
    "conservative x full cell area": (conservative, None),
    "bilinear": (bilinear, None),
    "nearest": (nearest, None),
})
source                            110,380 km3
conservative x covered area       110,380 km3    +0.0%
conservative x full cell area     144,855 km3   +31.2%
bilinear                           97,514 km3   -11.7%
nearest                           110,905 km3    +0.5%

Example 3 — how much of the source bilinear interpolation reads

def bilinear_footprint(src_lat, src_lon, dst_lat, dst_lon):
    """Share of source cells that bilinear interpolation to the target grid reads at all."""
    src_lat, src_lon = np.sort(src_lat), np.sort(src_lon)
    used = np.zeros((len(src_lat), len(src_lon)), dtype=bool)
    iy, ix = np.searchsorted(src_lat, dst_lat), np.searchsorted(src_lon, dst_lon)
    for a in (iy - 1, iy):
        for b in (ix - 1, ix):
            used[np.ix_(np.clip(a, 0, len(src_lat) - 1), np.clip(b, 0, len(src_lon) - 1))] = True
    print(f"bilinear reads {used.sum():,} of {used.size:,} source cells ({used.mean():.1%})")


for dlat, dlon in [(1.25, 1.875), (2.5, 2.5), (5.0, 5.0)]:
    bilinear_footprint(pr.lat.values, pr.lon.values,
                       np.arange(-90 + dlat / 2, 90, dlat), np.arange(dlon / 2, 360, dlon))
bilinear reads 110,592 of 259,200 source cells (42.7%)
bilinear reads 41,472 of 259,200 source cells (16.0%)
bilinear reads 10,368 of 259,200 source cells (4.0%)

Explanation

Why bilinear interpolation loses coastlines

Interpolating between four points needs all four. Where one is NaN — a sea cell next to land in a land-only product — the result is NaN. Coarsening multiplies the effect, because every target cell with sea anywhere near its centre becomes empty: the land area with data fell by 6.9% at N96. For fields with no gaps, such as a global temperature analysis, this particular loss does not happen.

Why conservative remapping needs the covered area

Conservative remapping preserves the integral of what it was given. For a masked field, the natural output is the mean over the valid part of each target cell, together with the size of that valid part. Dividing by covered area and later multiplying by full area applies the land mean to sea — the error is largest where cells are mostly sea, which is why the British Isles, a small land area in large cells, went up by 61%.

Why the choice depends on resolution as well as field

When refining, bilinear interpolation reads every source cell and produces a smooth field, while conservative remapping copies each source value into all the target cells inside it, producing blocks. When coarsening, the positions swap: bilinear samples and conservative averages. The same method can be the right choice in one direction and the wrong one in the other.

Why regridding cannot add information

A 5° cell built from 0.5° data by any method holds one number where there were 100. Refining a 2.5° grid to 0.5° produces 25 numbers from one, but no new detail: the extra resolution is interpolation, not measurement. Report the effective resolution of the source, not the grid you regridded to.

Bar chart of the share of source cells read by bilinear interpolation when regridding 0.5 degree data to the N96, 2.5 degree and 5 degree grids.
The coarser the target, the more of the source bilinear interpolation never looks at.

Edge cases or notes

  • Curvilinear and unstructured grids need general overlap weights; xESMF, CDO and similar tools compute them.
  • The longitude seam needs the same convention on both grids; convert first, as in longitude conventions explained.
  • Poles are points where longitude cells converge; clip edges to ±90°, as Example 1 does.
  • Second-order conservative remapping preserves totals and reduces blockiness when refining.
  • Vector fields such as wind components should be regridded together, not as independent scalars.
  • Categorical masks regridded with bilinear or conservative methods produce fractions; threshold them deliberately.
  • Precipitation extremes regridded conservatively are cell means; compare them only with extremes on the same grid.

FAQ

What is the difference between bilinear and conservative regridding?

Bilinear estimates each target cell from the four surrounding source centres; conservative averages every overlapping source cell by overlap area. Coarsening 2024 CPC precipitation to N96, conservative kept totals exact while bilinear lost 11.7%.

Which regridding method should I use for precipitation?

Conservative, because precipitation is a flux whose totals matter. Keep the area of valid data in each target cell and multiply by it when computing totals.

Why did regridding increase my precipitation total?

Probably because a masked field's regridded depths were multiplied by full cell areas. At N96 that inflated the global land total by 31.2%; multiplying by the covered area removed the error.

Why does bilinear interpolation produce NaN along coasts?

It needs all four neighbouring source cells, and any missing one makes the result NaN. For land-only data that removed 6.9% of the land area at N96.

Should I use nearest neighbour for land cover?

Yes, for categories, because it never creates invalid class values. When coarsening far, a majority rule over the overlapping cells represents the area better than the single nearest cell.

Does regridding to a finer grid add detail?

No. The extra cells are interpolated or copied from the source; the information content stays at the source resolution.