Session 2: sf Objects and Basic Mapping
sf object actually is, built from scratchsfg, sfc, and sfBy the end, you will map 6,460 real health establishments in Yucatán from a CSV.
Last week: coordinates, CRS, and reading a shapefile with st_read.
We said an sf object is a dataframe with a geometry column, and left it there.
Today we open it up. Knowing what is inside is what lets you fix it when it breaks.
Same folder shape as last week — code reads ../data/...:
ciencia_datos_2/
├── data/
│ ├── gadm41_MEX_shp/
│ ├── denue/denue_salud_yucatan.csv
│ └── mex_formats/
└── session2/
└── your_notes.qmd
Everything today runs from these two lines:
The complete, ordered code for this session is in session2_code.R.
sf Object IsThe most common vector files are:
| Type | Format | File extension |
|---|---|---|
| Binary | ESRI Shapefile | .shp, .shx, .dbf, .prj |
| GeoPackage (GPKG) | .gpkg |
|
| Plain Text | GeoJSON | .json or .geojson |
| GPS Exchange Format (GPX) | .gpx |
|
| Keyhole Markup Language (KML) | .kml |
|
| Spatial Databases | PostgreSQL / PostGIS |
spThe first R package for vector data was sp (2005–2023), with rgdal and rgeos.
They dominated spatial analysis in R for fifteen years. rgdal was retired in 2023.
Warning
Do not use them. But recognise them — most Stack Overflow answers you find were written for sp, and code using SpatialPolygonsDataFrame will not run today.
sf Packagesf is the standard package for working with vector data in R.
It relies on three external components: GDAL (reading formats), GEOS (geometry operations), and PROJ (projections).
It contains three classes, and they nest:
sfg — a single geometrysfc — a geometry column: many sfg plus a CRSsf — a layer: an sfc inside a dataframe of attributesEverything today is one of these three.
sf Class, IllustratedA polygon layer with three features and six non-spatial attributes:
sf Class, MappedThe same layer, drawn:
Attributes and geometry are two views of one object.
sfg is a single geometry, and it can be one of several types:
st_pointst_multipointst_linestringst_multilinestringst_polygonst_multipolygonst_geometrycollectionLet us create one point — the centre of Mérida:
Warning
Longitude first. c(-89.6237, 20.9674) is x then y. Swap them and Mérida lands in the Indian Ocean.
XY — two-dimensional geometryPOINT — the geometry typesfg — the general class: a simple feature geometryNote what is missing: there is no CRS. A bare sfg is just numbers.
The first and last coordinate are the same — that is what closes the ring. Omit it and sf errors.
ggplotgeom_sf accepts a bare sfg. You rarely do this in practice — but it shows the geometry is the only thing ggplot needs.
Same function, more vertices. A municipio boundary is this with a few thousand more.
We combine geometries with c():
The result is a single MULTIPOLYGON — one geometry composed of several shapes.
This is exactly what a state like Baja California Sur is: one feature, many islands.
New geometries can be calculated from existing ones:
Note the difference:
c(a, b) gives a MULTIPOLYGON — everything in eitherst_intersection(a, b) gives what is in bothIntersection is the workhorse of Session 5. Here we just meet it.
sfcA single geometry is rarely useful. We collect them into a geometry column with st_sfc.
Three points in Yucatán:
An sfc — unlike an sfg — carries a CRS.
4326crs object, as returned by st_crs()'+proj=longlat +datum=WGS84 +no_defs'Use the EPSG code. It is shortest and least error-prone.
Warning
If you omit the crs argument, the CRS is set to NA — undefined. The object still works, still plots, and then fails the moment you combine it with anything else.
sfcGeometry set for 3 features
Geometry type: POINT
Dimension: XY
Bounding box: xmin: -89.6636 ymin: 20.6896 xmax: -88.202 ymax: 21.282
Geodetic CRS: WGS 84
Notice what printing gives you now: geometry type, dimension, bounding box, and CRS. None of that existed on the bare sfg.
sfcThree points, no attributes. Useful, but we cannot yet say which is which.
sfc to sf: Adding AttributesSimple feature collection with 3 features and 2 fields
Geometry type: POINT
Dimension: XY
Bounding box: xmin: -89.6636 ymin: 20.6896 xmax: -88.202 ymax: 21.282
Geodetic CRS: WGS 84
city population combined
1 Mérida 995129 POINT (-89.6237 20.9674)
2 Valladolid 85460 POINT (-88.202 20.6896)
3 Progreso 63400 POINT (-89.6636 21.282)
st_sf() welds an attribute table to a geometry column. That is all an sf object is.
plot()Base plot() draws one panel per attribute — here, city and population.
ggplot()plot() or ggplot()?plot()
ggplot()
Tip
Use plot() while you work, ggplot() for anything anyone else will see. Getting into the habit of plot()-ing a layer the moment you read it catches most data problems in two seconds.
Sometimes you need to go the other way and pull out:
Geometry set for 3 features
Geometry type: POINT
Dimension: XY
Bounding box: xmin: -89.6636 ymin: 20.6896 xmax: -88.202 ymax: 21.282
Geodetic CRS: WGS 84
This gives you back the sfc — geometry and CRS, no attributes.
A plain dataframe. Use this whenever you want to inspect, summarise, or export attributes without dragging thousands of coordinates along.
Most data does not arrive as a shapefile. It arrives as a spreadsheet with a latitude column and a longitude column.
Turning that into a map is one function: st_as_sf().
DENUE — Directorio Estadístico Nacional de Unidades Económicas — is INEGI’s register of every economic establishment in Mexico, with coordinates.
Today’s extract: every health establishment in Yucatán.
[1] 6460 15
The two columns that matter: longitud and latitud.
st_as_sf(): Table to Layer[1] "sf" "data.frame"
Fifteen columns became fourteen: longitud and latitud were consumed and replaced by one geometry column.
Warning
coords = c("longitud", "latitud") — x first, then y. And always pass crs = 4326; without it the layer is undefined.
Six thousand dots. You can see Mérida, and you can guess at the coast — but only if you already know Yucatán.
#Step1: Read municipio polygons (GADM level 2)
mex <- st_read(
"../data/gadm41_MEX_shp/gadm41_MEX_2.shp",
quiet = TRUE)
#Step2: Keep only Yucatán
yuc <- mex[mex$NAME_1 == "Yucatán", ]
#Step3: Polygons first, points on top
ggplot() +
geom_sf(data = yuc,
fill = "#eef3f8",
colour = "#9db2c6",
linewidth = 0.15) +
geom_sf(data = pts, size = 0.2,
alpha = 0.4,
colour = "#33475b") +
theme_bw()Same points. Now the concentration in Mérida, and the emptiness of the east, are visible facts rather than guesses.
sf Properties: Rows and ColumnsAll the base R you know still applies. The geometry column counts as one column.
sf Properties: Bounding BoxThe extreme coordinates: westernmost, southernmost, easternmost, northernmost.
Tip
st_bbox is your first sanity check. If the numbers are not roughly where Yucatán should be (about -90 to -87, 20 to 22), something is wrong — usually swapped coordinates.
sf Properties: CRSDegrees — because we set 4326. To measure anything in metres, st_transform(pts, 6372) first.
Only the establishments that list a telephone number:
[1] 6460
[1] 3584
Ordinary subset(). The geometry follows the rows automatically — you never subset the geometry yourself.
f1 <- ggplot() +
geom_sf(data = yuc, fill = "#eef3f8", colour = "#9db2c6", linewidth = 0.12) +
geom_sf(data = pts, size = 0.18, alpha = 0.4, colour = "#33475b") +
ggtitle(paste0("All establishments (", nrow(pts), ")")) +
theme_bw(base_size = 9)
f2 <- ggplot() +
geom_sf(data = yuc, fill = "#eef3f8", colour = "#9db2c6", linewidth = 0.12) +
geom_sf(data = with_phone, size = 0.18, alpha = 0.4, colour = "#33475b") +
ggtitle(paste0("With a telephone (", nrow(with_phone), ")")) +
theme_bw(base_size = 9)
f1 | f2Just over half. And the ones that drop out are not randomly distributed — a reminder that missing data is itself spatial.
Selecting columns works as it does on any dataframe — with one difference:
You asked for two columns and got three. The geometry column is sticky — it comes along unless you explicitly drop it.
[1] "nom_estab" "municipio"
[1] "data.frame"
Now it is an ordinary dataframe. Do this before write.csv, or before any operation that does not need the geometry — it is dramatically faster.
[1] 32
Remember: a shapefile is four files minimum. st_read points at the .shp, but silently needs the others beside it.
[1] 32
One file, plain text. Open it in a text editor and you can read the coordinates.
[1] 32
One file, binary, and it can hold many layers at once. st_layers() tells you what is inside.
| Use it when | |
|---|---|
| Shapefile | someone insists — it is the old standard |
| GeoJSON | small data, web maps, version control |
| GeoPackage | your default — one file, no size limit, multiple layers |
Warning
Shapefile limitations that will bite you: column names truncated to 10 characters, a 2 GB size cap, and no proper support for accented characters. Nombre_del_Municipio becomes Nombre_del.
st_write picks the format from the file extension. delete_dsn = TRUE overwrites; without it, a second run errors.
sf object is three nested things: sfg → sfc → sfsfc doesst_sf() welds attributes to geometry — that is the whole trickst_geometry, st_drop_geometry, st_coordinates take a layer apartst_as_sf(df, coords = c("lon", "lat"), crs = 4326) turns any table into a mapNext session: choropleths — joining data to boundaries, classification, colour, and interactive maps with leaflet.
Popescu Data Science II — Session 2: sf Objects and Basic Mapping