Session 5: Rasters — Continuous Surfaces and Zonal Statistics
terra and tidyterracrop, mask and extract — turning a surface into a column in your tableToday’s data: monthly temperature across Mexico, and the GADM state boundaries.
Two small GeoTIFFs for ../data/raster/, plus the GADM state boundaries you already know from Session 1:
Sessions 1–4 were all vectors. Today is the other half.
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.
| 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 3, 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 levelAlready cropped to Mexico. The global download is 37 MB; cropped, the two come to 300 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] 6789 14
x y Jan Feb Mar
211 -115.4167 32.75 12.50225 14.92875 17.32875
212 -115.2500 32.75 12.55275 15.18300 17.78650
213 -115.0833 32.75 13.02050 15.62750 18.26500
One row per cell, one column per layer. The 21,504 cells become 6,789 rows because as.data.frame drops the NA sea cells by default.
The same three January values, but nothing in there says where they are. Row 2, columns 19–21 is the location — extent and resolution turn an index into a coordinate.
Same data, two objects. RStudio offers you tavg_df as a spreadsheet — 6,789 obs. of 14 variables. For tavg it can only say Formal class SpatRaster, because there is no table in there to show you.
The same map twice. geom_raster() is plain ggplot2 — x, y, fill. Nothing in the right-hand panel knows it is a map.
The same 6,789 locations, stored three ways:
| what it stores | size | |
|---|---|---|
sf points |
a geometry per cell | 3,536 KB |
| data frame | x and y columns |
771 KB |
SpatRaster |
no coordinates at all | extent + resolution |
A raster’s cells are on a regular grid, so terra derives any cell’s coordinates from the extent and the resolution. Nothing positional is stored per cell — only the values.
Note
That is the whole trade. Vectors store where each thing is, because things sit anywhere. Rasters store only values, because the grid already says where. Everything else about rasters follows from this.
[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.
A raster is always a rectangle. 21,504 cells, of which 6,789 carry a value — 68% of this file is NA. Mexico is just the part that happens to have data in it.
plotterra’s plot is fast and needs no arguments. Use it constantly while you work.
We mapped January. Map July instead — then put the two months side by side. Which part of the country changes most?
Look hard at the legends on that last plot before you conclude anything.
ggplot + tidyterrageom_spatraster() is the raster equivalent of geom_sf(). Everything you know about scales and themes still applies.
sf PolygonsThe same map — now as 6,789 POLYGON features, each storing its own five corner coordinates. The values take 168 KB; the polygons take 5,150 KB, about 30 times more, to say exactly what extent and resolution already said.
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_temp <- mean(tavg)
names(annual_temp) <- "tavg"
#Step2: Seasonal range -- how much the year swings
temp_range <- max(tavg) - min(tavg)
names(temp_range) <- "temp_range"
#minmax() reads the range straight off the layer
round(minmax(annual_temp), 1) tavg
min 8.0
max 28.2
8.0 °C to 28.2 °C across the country.
map_annual_temp <- ggplot() + geom_spatraster(data = annual_temp) +
scale_fill_viridis_c(option = "inferno", na.value = NA) + ggtitle("Annual mean")
map_temp_range <- ggplot() + geom_spatraster(data = temp_range) +
scale_fill_viridis_c(option = "mako", na.value = NA) + ggtitle("Seasonal swing")
map_annual_temp + map_temp_rangeTwo completely different maps from the same twelve layers.
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.
filter() masks by values; masking by a shape still needs mask().
Build the mask, count the cells, then map them. Which part of the country survives?
Guess the number first. There are 6,789 land cells in total.
aggregate[1] 21504
[1] 0.167
aggregate[1] 2432
[1] 0.5
aggregate[1] 286
[1] 1.5
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.36143
3 3 21.49858
extract DidEvery cell inside a boundary collapses to its mean. On the same colour scale you can see the cost: the surface ran 8.0 to 28.2 °C, the 32 state means only 13.4 to 26.5 °C. Averaging eats both tails.
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 — and the next three are its neighbours. The whole southeastern lowland, none of it far above sea level.
One extract call, and every state has a climate variable. The ordering is altitude, not latitude — the four coolest are all on the central plateau.
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.165
A maximum difference of 0.165 °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.
Tlaxcala, the smallest state, against 18 km cells. terra::extract keeps the 13 cells whose centre falls inside and ignores the rest. exactextractr uses all 26 cells that touch it, each weighted by its share — and only 3 of those lie entirely within the state.
Same polygon, same raster, two defensible numbers 0.12 °C apart. Nothing is broken — the two functions were asked slightly different questions, and the smaller the polygon gets relative to the cell, the further apart their answers drift.
| 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.
x, y and values — with the coordinates left out, because the grid already says whereterra; raster is retired and most online answers are written for itmean()/max() work across themNA, so every summary needs na.rm = TRUEfilter() applies it — by value, never by shapefact = 9 the Sierra Madre is gone, but the map still looks like datacrop then mask: rectangle first, shape secondextract turns a surface into a column and you are back in Session 3 — with exactextractr when the polygons are not much bigger than the 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