Session 5: Rasters — Continuous Surfaces and Zonal Statistics
terraextract — the operation that turns a raster into a column in your tableToday’s data: temperature, elevation, and rainfall across Mexico.
New data in ../data/raster/ — three small GeoTIFFs, 320 KB in total.
Four sessions of vectors: points, lines, polygons. Discrete objects with sharp edges.
That works for a hospital, a road, a municipio.
It works badly for temperature, rainfall, elevation, population density, night-time lights — things that exist everywhere and change gradually.
For those you need the other data model.
The same river as a vector (left) and as a raster (right).
Strip away the jargon and a raster is three things:
From extent and matrix dimensions you get resolution: how much ground one cell covers.
Note
A raster has no “features” and no attribute table. There is nothing to join to by name — only by location.
The same extent can be divided into few big cells or many small ones.
| Resolution | One cell is | Good for |
|---|---|---|
| 10 arc-min (today) | ~18 km | national climate |
| 1 km | 1 km² | municipio analysis |
| 30 m (Landsat) | a city block | land cover |
| 10 cm (drone) | a paving stone | infrastructure |
Finer is not better — it is bigger. Halving the cell size quadruples the file.
| Format | Extension | Notes |
|---|---|---|
| GeoTIFF | .tif |
the default; universally readable |
| Cloud-Optimised GeoTIFF | .tif |
a GeoTIFF you can read partially, over HTTP |
| NetCDF | .nc |
many dimensions — climate time series |
| ASCII grid | .asc |
plain text, huge, avoid |
Use GeoTIFF unless something forces you not to.
| Package | Status |
|---|---|
raster |
the old standard — retired, still everywhere online |
terra |
its replacement, by the same author. Use this. |
stars |
for data cubes: many dimensions, time series |
exactextractr |
one job, done fast: zonal statistics |
Warning
Just like sp in Session 2, most raster answers you find online are written for raster. terra uses rast() where raster used raster(), and objects are SpatRaster, not RasterLayer.
WorldClim 2.1 — global climate surfaces interpolated from weather stations, free, at several resolutions.
mexico_tavg_monthly.tif — mean temperature, 12 layers, one per monthmexico_elevation.tif — metres above sea levelmexico_precip_annual.tif — annual rainfall in mmAlready cropped to Mexico. The global download is 37 MB; cropped, all three come to 320 KB.
class : SpatRaster
size : 112, 192, 12 (nrow, ncol, nlyr)
resolution : 0.1666667, 0.1666667 (x, y)
extent : -118.5, -86.5, 14.33333, 33 (xmin, xmax, ymin, ymax)
coord. ref. : lon/lat WGS 84 (EPSG:4326)
source : mexico_tavg_monthly.tif
names : Jan, Feb, Mar, Apr, May, Jun, ...
min values : 3.154, 4.25175, 5.23275, 7.44325, 9.8045, 9.08475, ...
max values : 27.126429, 27.564501, 29.026001, 31.155251, 32.101749, 31.525249, ...
112, 192, 12 — rows, columns, and 12 layers0.1667 degrees — about 18 km at this latitudeJan through Dec; a layer is like a columnTip
print() on a SpatRaster does not print the data — it prints the description. With 21,504 cells per layer that is a mercy.
[1] 112 192 12
[1] 0.1666667 0.1666667
[1] 12
[1] "Jan" "Feb" "Mar" "Apr"
Jan
Min. : 3.154
1st Qu.:11.342
Median :14.200
Mean :15.044
3rd Qu.:18.929
Max. :27.126
NAs :14715
NA cells are the sea — we masked everything outside Mexico when the file was made. Most raster cells in a real analysis are NA, and every function needs na.rm = TRUE.
plotterra’s plot is fast and needs no arguments. Use it constantly while you work.
ggplot + tidyterrageom_spatraster() is the raster equivalent of geom_sf(). Everything you know about scales and themes still applies.
One facet_wrap over twelve layers. The north swings hard between winter and summer; the Yucatán barely moves.
Rasters on the same grid can be added, subtracted, multiplied, compared — cell by cell.
#Step1: The annual mean across the 12 monthly layers
annual <- mean(tavg)
names(annual) <- "tavg"
#Step2: Seasonal amplitude -- how much the year swings
amplitude <- max(tavg) - min(tavg)
names(amplitude) <- "amplitude"
round(c(min = min(values(annual), na.rm = TRUE),
max = max(values(annual), na.rm = TRUE)), 1) min max
8.0 28.2
8.0 °C to 28.2 °C across the country.
Two completely different maps from the same twelve layers.
[1] 1130
[1] 6789
1,130 of 6,789 land cells average above 25 °C — about 17% of the country.
Warning
Cell counts are not area unless every cell is the same size. In EPSG:4326 they are not — a degree of longitude shrinks toward the poles. Project first if you need real areas.
aggregatecells_before cells_after
21504 2432
res_before res_after
0.167 0.500
21,504 cells become 2,432. Use this when a raster is finer than your question — it makes everything downstream faster.
The reverse is disagg(), which invents detail. It does not add information.
Now each cell is about 17 km × 17 km in real metres.
Important
Reprojecting a vector moves exact coordinates. Reprojecting a raster must build a new grid and interpolate values into it — so it changes your data. Reproject once, as late as possible, and use method = "near" for categorical rasters like land cover.
Two operations that sound alike and are not:
crop cuts the raster down to a rectangle — the bounding box of your vectormask sets cells outside the actual shape to NAYou almost always want crop then mask, in that order — cropping first makes the mask far cheaper.
extract: The Operation That MattersThis is why rasters are worth learning. extract turns a surface into a column in your table.
ID tavg
1 1 16.69675
2 2 18.36829
3 3 21.48210
state_name tavg
1 Tabasco 26.5
2 Campeche 26.0
3 Yucatán 25.8
4 Quintana Roo 25.7
Tabasco at 26.5 °C; Tlaxcala at 13.4 °C. Both are at similar latitudes — the difference is altitude.
We are back in Session 3. Once extracted, a raster variable is just another column to map, classify, and join.
An 18 km cell straddling a state line belongs partly to each. terra::extract includes a cell if its centre falls inside.
exactextractr weights each cell by the fraction of it that the polygon covers:
[1] 0.169
A maximum difference of 0.17 °C here — small, because our states are far larger than the cells. For municipios at this resolution the difference would be serious.
Tip
Rule of thumb: if your polygons are not many times bigger than your cells, use exactextractr.
[1] -0.906
A correlation of -0.91, and about 4.1 °C cooler per 1,000 metres.
The physical free-air lapse rate is about 6.5 °C per 1,000 m. We got 4.1. Which is wrong?
Neither. First, check that averaging 32 states did not distort the estimate — refit on all 6,789 raw cells:
-4.15 — essentially identical to the state-level -4.14. Averaging first was harmless here.
The gap is not an error. A surface lapse rate is not a free-air lapse rate.
Surface lapse rates are typically shallower. And it is not latitude doing it:
Tip
The habit that matters: when a number disagrees with a textbook, check whether you computed the same quantity before concluding your data is broken. Here, three specifications all give -4.1, which is a strong hint the estimate is fine and the comparison was not.
Sometimes you want to go the other way: thousands of points, summarised onto a grid.
#Step1: The DENUE health points from Sessions 2 and 4
d <- read.csv("../data/denue/denue_salud_yucatan.csv")
pts <- vect(st_as_sf(d, coords = c("longitud", "latitud"), crs = 4326))
#Step2: An empty grid to count into -- 0.02 degrees is about 2 km
tmpl <- rast(ext(-90.5, -87.4, 19.9, 21.7), resolution = 0.02, crs = "EPSG:4326")
#Step3: Count the points falling in each cell
counts <- rasterize(pts, tmpl, fun = "count", background = 0)
c(cells = ncell(counts), occupied = sum(values(counts) > 0), max = max(values(counts))) cells occupied max
13950 337 564
Counts run from 0 to 564, so the raw scale shows Mérida and nothing else. log1p — log of one plus the count — compresses that and handles the zeros.
| Cell size | Grid | Occupied cells | Busiest cell |
|---|---|---|---|
| ~6 km | 36 × 62 | 200 | 1,478 |
| ~2 km | 90 × 155 | 337 | 564 |
Same points, same state. The grid you choose decides how concentrated the pattern looks.
Warning
This is the modifiable areal unit problem, and it applies to municipios exactly as much as to grid cells. There is no neutral choice — only a stated one.
Do rasterize when you need to combine points with other continuous surfaces, or when your units are arbitrary anyway.
Do not rasterize when a real administrative unit is the thing you care about. A municipio is a budget and a mayor; a grid cell is not.
For the reto: rasterizing is usually a step on the way to an extract, not an output in itself.
near, bilinear, average)All are in terra, and all follow the patterns you have now seen.
| Source | What | Resolution |
|---|---|---|
| WorldClim | climate normals | 1 km – 20 km |
| WorldPop | population counts | 100 m – 1 km |
| SRTM / Copernicus DEM | elevation | 30 m |
| VIIRS night lights | economic activity proxy | 500 m |
| INEGI / CONABIO | land use, vegetation | varies |
For the reto, WorldPop is the one to reach for — it gives you the population denominator we kept wishing for in Session 3.
terra; raster is retired and most online answers are written for itmean()/max() work across themcrop then mask: rectangle first, shape secondextract turns a surface into a column, and then you are back in Session 3exactextractr when polygons are not much bigger than cellsThat is the course. You can now read, join, measure, classify, map, and extract — which is most of applied spatial analysis.
Popescu Data Science II — Session 5: Rasters