How to Choose a Cell Size for an Interpolated Surface
Problem statement
The output cell size is the one parameter of an interpolation that has no right answer in the data, so people default to whatever looks smooth β usually far finer than the samples support.
It is easy to measure what that costs. Five hundred samples over a 105 kmΒ² area, interpolated to grids of different cell sizes, scored against the real elevation surface:
cell size output cells RMSE
10 m 1,045,233 54.01 m
15 m 464,548 54.00 m
30 m 116,137 53.96 m
60 m 28,864 53.90 m
120 m 7,216 53.89 m
250 m 1,638 54.17 m
The 10 m grid has 145 times as many cells as the 120 m grid and is very slightly less accurate. Every one of those extra million cells is interpolation, not information.
Quick answer
Start from the mean sample spacing and go one step finer, no more:
import numpy as np
def suggested_cell_size(n_samples, area_m2, factor=0.5):
"""Mean sample spacing, halved. Finer than this adds cells, not information."""
spacing = np.sqrt(area_m2 / n_samples)
cell = spacing * factor
print(f" {n_samples} samples over {area_m2 / 1e6:.1f} kmΒ²")
print(f" mean spacing {spacing:7.0f} m")
print(f" suggested cell size {cell:7.0f} m")
print(f" grid would be {area_m2 / cell ** 2:,.0f} cells")
return cell
500 samples over 104.5 kmΒ²
mean spacing 457 m
suggested cell size 228 m
grid would be 2,009 cells
Step-by-step solution
1. Compute the mean sample spacing
spacing = np.sqrt(area / n_samples)
This is the spacing a perfect grid of the same size would have. With 500 samples over 104.5 kmΒ² it is 457 m β the natural resolution of this survey, regardless of what grid you write it to.
2. Pick a cell size at or below half that spacing
Half the spacing is a reasonable convention: fine enough that the grid does not itself introduce visible blockiness, coarse enough that you are not storing a hundred cells per observation.
At 457 m spacing that gives about 230 m cells. If that sounds shockingly coarse, that is the correct reaction to what 500 samples over 105 kmΒ² actually supports.
3. Do not confuse cell size with accuracy
The temptation is to reason "a 10 m DEM is better than a 100 m DEM, so I should output 10 m". That holds when the measurements are at 10 m. It does not hold when the measurements are 457 m apart and the 10 m grid is an interpolation.
The measured RMSE moved by 0.28 m β half a percent β across a 25Γ range of cell sizes. Meanwhile the file grew by a factor of 640.
4. Match the neighbours' grid if you will combine layers
If the surface will be stacked with other rasters, matching their grid saves a resampling step and prevents alignment bugs. That is often a better reason to pick a cell size than any property of the samples.
Pick the coarsest resolution among the layers you will combine, and snap the origin so pixel corners line up. See How to resample satellite bands to a common grid for the alignment mechanics.
5. Consider what happens downstream
Some operations impose their own constraint:
- Zonal statistics over small polygons need cells small enough that each polygon contains several. A 230 m cell is useless for field-scale parcels.
- Slope and aspect are extremely sensitive to cell size, because they are derivatives.
- Visual products benefit from finer cells even when accuracy does not, because contour lines and hillshades look ragged on coarse grids.
Those are legitimate reasons to go finer. "The map looks smoother" is a legitimate reason too β as long as nobody reads the fine grid as fine data.
Code examples
Example 1 β a cell-size sweep on your own data
import numpy as np
from scipy.spatial import cKDTree
def cell_size_sweep(points, values, bounds, truth_fn,
candidates=(10, 25, 50, 100, 250, 500), power=2, k=12):
"""Interpolate at several cell sizes and score each against known truth."""
left, bottom, right, top = bounds
tree = cKDTree(points)
for cell in candidates:
width = int((right - left) / cell)
height = int((top - bottom) / cell)
xs = left + (np.arange(width) + 0.5) * cell
ys = top - (np.arange(height) + 0.5) * cell
gx, gy = np.meshgrid(xs, ys)
grid = np.column_stack([gx.ravel(), gy.ravel()])
d, i = tree.query(grid, k=min(k, len(points)))
d = d.reshape(len(grid), -1); i = i.reshape(len(grid), -1)
w = 1.0 / np.maximum(d, 1e-12) ** power
predicted = (w * values[i]).sum(1) / w.sum(1)
truth = truth_fn(grid)
rmse = float(np.sqrt(np.mean((predicted - truth) ** 2)))
print(f" {cell:4d} m {len(grid):10,} cells RMSE {rmse:7.2f} "
f"{len(grid) * 4 / 1e6:7.1f} MB as float32")
You need truth to run this, which means a pilot on a dense dataset. Do it once for your variable and landscape and the answer transfers.
Example 2 β deriving the cell size from the variogram instead
import numpy as np
def cell_size_from_variogram(nugget, sill, rng, n_samples, area_m2):
"""Two independent limits on useful resolution."""
spacing = np.sqrt(area_m2 / n_samples)
# 1. sampling limit: you cannot resolve detail finer than the spacing
sampling_limit = spacing / 2
# 2. structure limit: features shorter than about a tenth of the range
# are not represented in the model at all
structure_limit = rng / 10
chosen = max(min(sampling_limit, structure_limit), 1.0)
print(f" sampling limit {sampling_limit:7.0f} m (spacing {spacing:.0f} m)")
print(f" structure limit {structure_limit:7.0f} m (range {rng:.0f} m)")
print(f" suggested {chosen:7.0f} m")
if nugget / (nugget + sill) > 0.3:
print(f" ! nugget is {nugget / (nugget + sill):.0%} of the sill β much of "
"the variation is unresolvable at any cell size")
return chosen
sampling limit 228 m (spacing 457 m)
structure limit 454 m (range 4537 m)
suggested 228 m
The two limits usually disagree, and the smaller one wins. Here sampling is the binding constraint β the structure extends over kilometres, so it is the survey, not the phenomenon, that sets the resolution.
Example 3 β writing a coarse surface without a coarse-looking map
import numpy as np
import rasterio
from rasterio.enums import Resampling
def write_with_display_overview(path, surface, profile, display_cell=None):
"""Store at the honest resolution; resample only for display, and say so."""
with rasterio.open(path, "w", **profile) as dst:
dst.write(surface, 1)
dst.build_overviews([2, 4, 8], Resampling.average)
dst.update_tags(
resolution_note="stored at the resolution the sampling supports; "
"any finer rendering is interpolation for display",
)
if display_cell:
factor = profile["transform"].a / display_cell
print(f" display copy would be {factor:.0f}x finer β "
"render it, do not store it as data")
If a stakeholder wants a smooth 10 m map, produce it as a rendering rather than as a raster of numbers. The moment a fine grid exists as data, someone will run zonal statistics on it and quote the result to three decimal places.
Explanation
Why accuracy is flat across cell sizes
An interpolated value at a location depends only on the samples near it. Two adjacent cells of a fine grid have almost the same nearest samples and almost the same weights, so they get almost the same value.
Refining the grid therefore does not add information; it adds cells that are nearly copies of their neighbours. The RMSE stays put because the surface is the same surface, sampled more densely.
The very slight worsening at the extremes has two separate causes. At 250 m, cells become large enough that a single value is a poor summary of the ground it covers. At 10 m, the surface faithfully reproduces the bullseyes around each sample, which are artefacts β a coarser grid averages some of them away.
Why fine grids are actively harmful
Three costs, none of them obvious at the moment you type the number:
- Storage and speed. 145Γ the cells is 145Γ the bytes and roughly 145Γ the time for every downstream operation, forever.
- False precision. A 10 m raster invites 10 m questions. Someone will extract a value for a single parcel and treat it as a measurement of that parcel.
- Hidden artefacts. Bullseyes and triangulation facets are much more visible at fine resolution β which is at least honest, but they will be read as terrain.
Why the sampling limit is usually the binding one
Two things limit useful resolution: how densely you sampled, and how fine the real structure is. The measurement above has a variogram range of 4,537 m β the phenomenon varies over kilometres β while the sample spacing is 457 m.
So the survey is comfortably dense relative to the structure, and the resolution is set by the sampling. That is the normal situation. The reverse β structure finer than your samples can see β is the case where no cell size helps, because the variation is inside the nugget.
Why this matters more than the interpolation method
The method choice was worth 25% here (kriging against IDW). The cell size choice is worth 0% in accuracy and a factor of 640 in file size.
Getting the method wrong costs you some accuracy. Getting the cell size wrong costs you nothing measurable and everything in credibility, because the output claims a resolution the data cannot support.
Edge cases or notes
sqrt(area / n)assumes even spacing. For clustered samples use the 95th-percentile gap instead; it is a much larger number.- Snap the origin to a multiple of the cell size so grids at different resolutions share pixel corners.
- Slope and aspect are derivatives and change substantially with cell size β do not compute them from an over-fine interpolated grid.
- Zonal statistics need several cells per polygon. If that forces a fine grid, the honest conclusion may be that the sampling cannot answer the question.
- Store coarse, render fine. Overviews and display resampling do not lie about resolution; a fine raster does.
- Match neighbouring layers if the surface will be stacked β often the strongest argument.
- A large nugget caps useful resolution regardless of sampling density.
- Record the sample spacing in the file tags, so the next person can see what the grid is standing on.
Internal links
- Spatial interpolation explained β the error-versus-distance picture
- The variogram explained β the range and nugget used above
- Sample design explained: where to measure β the spacing that sets the limit
- How to interpolate points to a grid with IDW in Python β where the cell size is set
- My IDW surface is full of bullseyes β the artefact a fine grid renders faithfully
- How to calculate slope and aspect from a DEM in Python β why derivatives are cell-size sensitive
- Raster resampling explained β the mechanics of changing cell size
- How to calculate zonal statistics in Python β the downstream operation that pushes for fine cells
FAQ
What cell size should I use for an interpolated surface?
Around half the mean sample spacing, sqrt(area / n) / 2. With 500 samples over 105 kmΒ² that is about 230 m.
Does a finer grid make the surface more accurate?
No. Across cell sizes from 10 m to 250 m the RMSE moved by 0.28 m β half a percent β while the cell count changed by a factor of 640.
Why does my 10 m interpolated raster look so much better?
Because it is smoother to look at. Smoothness is a property of the rendering, not of the information; the 10 m grid has 45 cells between adjacent observations and 44 of them are model output.
Should I match the cell size of my other rasters?
Usually yes. Alignment saves a resampling step and prevents a whole class of bugs, and it is often a better criterion than anything in the samples themselves.
What if I need fine cells for zonal statistics?
Then check whether the sampling supports the question. If polygons are smaller than the sample spacing, a finer grid gives you a number per polygon but not information per polygon.
How does the variogram help choose a cell size?
It gives a second limit: features shorter than about a tenth of the range are not in the model. Take the smaller of that and half the sample spacing.
Can I store coarse and display fine?
Yes, and you should. Build overviews and let the renderer interpolate. That keeps the data honest while the map looks the way people expect.