Week 8: The Structure of sf Objects

PPOL 6805: GIS for Spatial Data Science

Workshop Sessions
Author

Christy Hsu

Published

October 16, 2026

R Coding Workshop for GIS: 7th Meeting

Goals for Today

  • Review R fundamentals
  • Walk through the R workflow for (geospatial) data analysis
  • Make sense of objects and functions in R
  • Move back and forth with df, sf, ppp
  • Intro to control flow (if and case_when)

Review

  1. Everything that exists in R is an object.(Chambers (2016))1
  2. R uses functions to perform operations.(James (2021))2

The Workflow 1: Setting up the Environment

  • select a working environment and explore their features(Positron, Jupyterhub)
  • Open your .qmd file or create one

R Session

  • Use getwd() and setwd() to get information about working directory the or set them manually to tell R where to look for files during this session
getwd()
setwd('~/myfolder')
  • Use the library() function to load the packages for our R session.
library(tidyverse)
library(sf)
library(mapview)
library(rnaturalearth)
library(viridis)
library(spdep)
library(sfdep)
library(spatstat)
library(terra)
library(stars)

The Workflow 2: Preparing Data objects

uni_df <- tibble::tribble(
  ~university, ~year, ~current_country, ~lat, ~lon, ~exist_today,
  "Paris", 1150, "France", 48.8566, 2.3522, TRUE,
  "Salerno", 1173,  "Italy", 40.7711, 14.7905, TRUE,
  "Reggio", 1188, "Italy", 44.6450, 10.9277, TRUE,
  "Oxford", 1190, "United Kingdom", 51.7520, -1.2576, TRUE,
  "Bologna", 1200, "Italy", 44.4989, 11.3275, TRUE
)
# uni_df
uni_sf <- uni_df |>
    st_as_sf(
        coords = c('lon','lat'),
        crs = 4326
    )
# uni_sf |> mapview(label = 'university')
uni_sfc <- uni_sf |> pull(geometry)
book_challenge_df <- read_csv('data/book-challenge11.csv')
Rows: 931 Columns: 17
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr   (3): title, author, state
dbl  (13): book_id, year, removed, explicit, antifamily, occult, language, l...
date  (1): date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
ne_vars <- c("geounit", "pop_est", "pop_rank", "pop_year", "gdp_md", "gdp_year", "economy", "income_grp", "iso_a2", "continent", "region_un", "subregion", "region_wb")
cca_sf <- ne_countries(
    country = c(
        "Kazakhstan", "Kyrgyzstan", "Tajikistan", "Turkmenistan", "Uzbekistan", "Armenia", "Azerbaijan", "Georgia"),
    scale = 50) |> select(any_of(ne_vars))
cca_sfc <- cca_sf |> st_geometry()
azerbaijan_sf <- cca_sf |> filter(iso_a2 == 'AZ')
azerbaijan_sfc <- azerbaijan_sf |> st_geometry()
georgia_sf <- cca_sf |> filter(iso_a2 == 'GE')
cca_nb <- cca_sf |> st_contiguity()
Warning in spdep::poly2nb(geometry, queen = queen, ...): neighbour object has 2 sub-graphs;
if this sub-graph count seems unexpected, try increasing the snap argument.
cca_listw <- cca_nb |> st_weights()

3. Finding Help: read function documentations

Ways to access function documentation:

  1. Add a question mark in front of the function name: ?func_name
?mutate
??"aggregate"
  1. Use the help() function
help('mutate')
help('filter', package = 'dplyr')
  1. If you are interested in the what are the arguments that the function can take, use args() to check
moran |> class() |> print()
[1] "function"
args(moran)
function (x, listw, n, S0, zero.policy = attr(listw, "zero.policy"), 
    NAOK = FALSE) 
NULL
  1. Simply, hover over the function name!
'Try hover over a function in your code!' |> print()
[1] "Try hover over a function in your code!"

A closer look at the data structure of an df object

  • class() function tells us that uni_df is a “data.frame” object data object with an additional class `“tbl_df”
  • typeof() call shows the storage mode of uni_df, which is list
uni_df |> class() |> print()
[1] "tbl_df"     "tbl"        "data.frame"
uni_df |> typeof() |> print()
[1] "list"
  • describe and extract attributes of a df object using dim() and colnames()
uni_df |> dim() |> print()
[1] 5 6
uni_df |> colnames()
[1] "university"      "year"            "current_country" "lat"            
[5] "lon"             "exist_today"    

Longer and Wider Dataframes

book_challenge_df <- read_csv('data/book-challenge11.csv')
Rows: 931 Columns: 17
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr   (3): title, author, state
dbl  (13): book_id, year, removed, explicit, antifamily, occult, language, l...
date  (1): date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
book_challenge_df |> dim() |> print()
[1] 931  17
  • basic display functions: head(), tail() and slice_sample()
book_challenge_df |> tail()
title book_id author date year removed explicit antifamily occult language lgbtq violent state political_value_index median_income hs_grad_rate college_grad_rate
And Tango Makes Three 143 Parnell, Peter and Justin Richardson 2006-10-29 2006 0 0 0 0 0 1 0 WV -7.9 -7290.0 -4.361958 -9.22373
Go and Come Back 721 Abelove, Joan 2000-06-18 2000 0 1 0 0 0 0 0 WV -7.9 -7290.0 -4.361958 -9.22373
When Dad Killed Mom 1964 Lester, Julius 2002-04-21 2002 1 0 0 0 0 0 1 WY -19.7 4081.5 8.338042 -2.12373
Geology Book, The 755 Morris, Dr. John D. 2010-05-05 2010 0 0 0 0 0 0 0 WY -19.7 4081.5 8.338042 -2.12373
And Tango Makes Three 143 Parnell, Peter and Justin Richardson 2009-08-22 2009 0 0 0 0 0 1 0 WY -19.7 4081.5 8.338042 -2.12373
Darkest Night of the Year 505 Koontz, Dean 2007-12-03 2007 0 1 0 0 0 0 0 WY -19.7 4081.5 8.338042 -2.12373
book_challenge_df |> slice_sample(n = 4)
title book_id author date year removed explicit antifamily occult language lgbtq violent state political_value_index median_income hs_grad_rate college_grad_rate
Mars, Vol. 10 1236 Soryo, Fuyumi 2008-01-29 2008 0 1 0 0 0 0 0 MI 3.8 2966.0 3.8380421 -2.22373
Your Move 2055 Bunting, Eve 2008-04-02 2008 0 0 0 0 0 0 0 OK -16.9 -3087.5 1.0380421 -3.72373
Eat Me 610 Javin, Linda 2003-10-12 2003 1 1 0 0 0 0 0 FL -1.8 372.0 0.3380421 -1.72373
Timespan Spies 1814 Healey, Tim 2001-04-25 2001 0 0 0 0 0 0 1 CA 7.4 10119.0 -2.7619579 2.57627
  • optimized display to show all columns glimpse()
book_challenge_df |> glimpse()
Rows: 931
Columns: 17
$ title                 <chr> "House of the Spirits, The", "It's Not the Stork…
$ book_id               <dbl> 927, 1024, 1087, 936, 764, 1087, 1489, 2023, 102…
$ author                <chr> "Allende, Isabel", "Harris, Robie", "Pyle, Howar…
$ date                  <date> 2005-04-01, 2008-02-06, 2008-10-02, 2008-10-05,…
$ year                  <dbl> 2005, 2008, 2008, 2008, 2008, 2003, 2003, 2003, …
$ removed               <dbl> 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, …
$ explicit              <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, …
$ antifamily            <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
$ occult                <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ language              <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, …
$ lgbtq                 <dbl> 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
$ violent               <dbl> 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, …
$ state                 <chr> "AK", "AK", "AK", "AK", "AK", "AK", "AK", "AK", …
$ political_value_index <dbl> -13.4, -13.4, -13.4, -13.4, -13.4, -13.4, -13.4,…
$ median_income         <dbl> 15707.5, 15707.5, 15707.5, 15707.5, 15707.5, 157…
$ hs_grad_rate          <dbl> 8.738042, 8.738042, 8.738042, 8.738042, 8.738042…
$ college_grad_rate     <dbl> 0.6762701, 0.6762701, 0.6762701, 0.6762701, 0.67…
  • view() tidyverse summary() equivalent
book_challenge_df |> view()

The structure and the components of an sf object

  • an sf object can simultaneously be of classes “sf”, “tbl_df” and “data.frame”
  • Using the function typeof() or mode() we can inspect that uni_sf is stored as using a list
uni_sf |> class()
[1] "sf"         "tbl_df"     "tbl"        "data.frame"
uni_sf |> typeof()
[1] "list"

Attributes of an sf object

  • df attributes
  • sf column
    • coordinate reference system: st_crs()
    • bounding box: st_bbox()
    • geometry type
  • attribute-geometry-relationship
uni_sf
university year current_country exist_today geometry
Paris 1150 France TRUE POINT (2.3522 48.8566)
Salerno 1173 Italy TRUE POINT (14.7905 40.7711)
Reggio 1188 Italy TRUE POINT (10.9277 44.645)
Oxford 1190 United Kingdom TRUE POINT (-1.2576 51.752)
Bologna 1200 Italy TRUE POINT (11.3275 44.4989)
uni_sf |> str()
sf [5 × 5] (S3: sf/tbl_df/tbl/data.frame)
 $ university     : chr [1:5] "Paris" "Salerno" "Reggio" "Oxford" ...
 $ year           : num [1:5] 1150 1173 1188 1190 1200
 $ current_country: chr [1:5] "France" "Italy" "Italy" "United Kingdom" ...
 $ exist_today    : logi [1:5] TRUE TRUE TRUE TRUE TRUE
 $ geometry       :sfc_POINT of length 5; first list element:  'XY' num [1:2] 2.35 48.86
 - attr(*, "sf_column")= chr "geometry"
 - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA
  ..- attr(*, "names")= chr [1:4] "university" "year" "current_country" "exist_today"
uni_sf |> st_crs()
Coordinate Reference System:
  User input: EPSG:4326 
  wkt:
GEOGCRS["WGS 84",
    ENSEMBLE["World Geodetic System 1984 ensemble",
        MEMBER["World Geodetic System 1984 (Transit)"],
        MEMBER["World Geodetic System 1984 (G730)"],
        MEMBER["World Geodetic System 1984 (G873)"],
        MEMBER["World Geodetic System 1984 (G1150)"],
        MEMBER["World Geodetic System 1984 (G1674)"],
        MEMBER["World Geodetic System 1984 (G1762)"],
        MEMBER["World Geodetic System 1984 (G2139)"],
        MEMBER["World Geodetic System 1984 (G2296)"],
        ELLIPSOID["WGS 84",6378137,298.257223563,
            LENGTHUNIT["metre",1]],
        ENSEMBLEACCURACY[2.0]],
    PRIMEM["Greenwich",0,
        ANGLEUNIT["degree",0.0174532925199433]],
    CS[ellipsoidal,2],
        AXIS["geodetic latitude (Lat)",north,
            ORDER[1],
            ANGLEUNIT["degree",0.0174532925199433]],
        AXIS["geodetic longitude (Lon)",east,
            ORDER[2],
            ANGLEUNIT["degree",0.0174532925199433]],
    USAGE[
        SCOPE["Horizontal component of 3D system."],
        AREA["World."],
        BBOX[-90,-180,90,180]],
    ID["EPSG",4326]]
uni_sf |> st_bbox() |> st_as_sfc() |> st_as_sf() |> mapview()

Geometry column: a closer look

  1. Get the name of the geometry column
uni_sf |> attr(which = 'sf_column')
[1] "geometry"
  1. Single out the geometry column
  • pull() tidyverse equivalent for $: input a df, a column name, output a vector
uni_sf |> pull(geometry) |> class()
[1] "sfc_POINT" "sfc"      
uni_sfc <- uni_sf |> pull(geometry)
# uni_sfc <- uni_sf$geometry
  • or st_geometry()
uni_sfc <- uni_sf |> st_geometry()
uni_sfc |> class() |> print()
[1] "sfc_POINT" "sfc"      
uni_sfc |> typeof()
[1] "list"
  • access list items
uni_sfc[[4]]
POINT (-1.2576 51.752)
uni_sfc[[4]] |> class()
[1] "XY"    "POINT" "sfg"  

summary

  1. sf: simple feature collection.
uni_sf
university year current_country exist_today geometry
Paris 1150 France TRUE POINT (2.3522 48.8566)
Salerno 1173 Italy TRUE POINT (14.7905 40.7711)
Reggio 1188 Italy TRUE POINT (10.9277 44.645)
Oxford 1190 United Kingdom TRUE POINT (-1.2576 51.752)
Bologna 1200 Italy TRUE POINT (11.3275 44.4989)
  1. sfc: simple feature list-column(Pebesma and Bivand (2023))
uni_sfc
Geometry set for 5 features 
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -1.2576 ymin: 40.7711 xmax: 14.7905 ymax: 51.752
Geodetic CRS:  WGS 84
POINT (2.3522 48.8566)
POINT (14.7905 40.7711)
POINT (10.9277 44.645)
POINT (-1.2576 51.752)
POINT (11.3275 44.4989)
  1. sfg: single geometry
uni_sfc[[4]]
POINT (-1.2576 51.752)

st_geometry: make use of the list structure

azerbaijan_sf <- cca_sf |> filter(iso_a2 == 'AZ')
azerbaijan_sf |> mapview()
azerbaijan_sf |> glimpse()
Rows: 1
Columns: 14
$ geounit    <chr> "Azerbaijan"
$ pop_est    <dbl> 10023318
$ pop_rank   <int> 14
$ pop_year   <int> 2019
$ gdp_md     <int> 48047
$ gdp_year   <int> 2019
$ economy    <chr> "6. Developing region"
$ income_grp <chr> "3. Upper middle income"
$ iso_a2     <chr> "AZ"
$ continent  <chr> "Asia"
$ region_un  <chr> "Asia"
$ subregion  <chr> "Western Asia"
$ region_wb  <chr> "Europe & Central Asia"
$ geometry   <MULTIPOLYGON [°]> MULTIPOLYGON (((44.81719 39...
  • save the sf geometry column as azerbaijan_sfc
azerbaijan_sfc <- azerbaijan_sf |> st_geometry()
azerbaijan_sfc |> glimpse()
sfc_MULTIPOLYGON of length 1; first list element: List of 3
 $ :List of 1
  ..$ : num [1:41, 1:2] 44.8 44.8 44.8 44.9 45 ...
 $ :List of 2
  ..$ : num [1:206, 1:2] 48.9 48.8 48.6 48.6 48.4 ...
  ..$ : num [1:8, 1:2] 45.6 45.6 45.5 45.5 45.5 ...
 $ :List of 1
  ..$ : num [1:9, 1:2] 45 45 45 45 45 ...
 - attr(*, "class")= chr [1:3] "XY" "MULTIPOLYGON" "sfg"
azerbaijan_sfc |> class() |> print()
[1] "sfc_MULTIPOLYGON" "sfc"             
azerbaijan_sfc |> typeof() |> print()
[1] "list"
azerbaijan_sfc[[1]][[3]][[1]] |> class()
[1] "matrix" "array" 
azerbaijan_sfc[[1]][[3]][[1]]
          [,1]     [,2]
 [1,] 45.02363 41.02725
 [2,] 45.00205 41.01582
 [3,] 44.96904 41.02725
 [4,] 44.95889 41.05264
 [5,] 44.96143 41.07925
 [6,] 44.99434 41.08560
 [7,] 45.02109 41.07798
 [8,] 45.02871 41.05386
 [9,] 45.02363 41.02725
az_poly1 <- azerbaijan_sfc[[1]][[1]] |> st_polygon()
az_poly2 <- azerbaijan_sfc[[1]][[2]] |> st_polygon()
az_poly3 <- azerbaijan_sfc[[1]][[3]] |> st_polygon()

azer_piece_sfc <- list(az_poly1, az_poly2, az_poly3) |>
  st_as_sfc()
azer_piece_df <- tibble(
    geometry = azer_piece_sfc,
    lab = c('Nagorno-Karabakh', 'Azerbaijan Main', 'Yukhari Askipara')
)
azer_piece_sf <- azer_piece_df |> st_as_sf(crs = 4326)
azer_piece_sf |> mapview(zcol = 'lab', label = 'lab')
azer_piece_sf
geometry lab
POLYGON ((44.81719 39.65044… Nagorno-Karabakh
POLYGON ((48.86875 38.4355,… Azerbaijan Main
POLYGON ((45.02363 41.02725… Yukhari Askipara

sfg to sfc

Inner Ring
azerbaijan_sfc[[1]][[2]][[2]] |> class() |> print()
[1] "matrix" "array" 
azerbaijan_sfc[[1]][[2]][[2]]
         [,1]     [,2]
[1,] 45.55234 40.61606
[2,] 45.56230 40.64917
[3,] 45.53418 40.66401
[4,] 45.50449 40.66484
[5,] 45.47881 40.64834
[6,] 45.47881 40.60698
[7,] 45.51436 40.59956
[8,] 45.55234 40.61606
azerbaijan_sfc[[1]][[2]][[2]] |> list() |> st_polygon() |> ggplot() + geom_sf() + theme_bw()

Outer Ring

azerbaijan_sfc[[1]][[2]][[1]] |> dim() |> print()
[1] 206   2
azerbaijan_sfc[[1]][[2]][[1]] |> head()
         [,1]     [,2]
[1,] 48.86875 38.43550
[2,] 48.84033 38.43726
[3,] 48.63555 38.39873
[4,] 48.59268 38.41108
[5,] 48.41738 38.58623
[6,] 48.38125 38.60562
azerbaijan_sfc[[1]][[2]][[1]] |> list() |> st_polygon() |> ggplot() + geom_sf() + theme_bw()

azerbaijan_outer_ring <- azerbaijan_sfc[[1]][[2]][[1]]
azerbaijan_inner_ring <- azerbaijan_sfc[[1]][[2]][[2]]


azerbaijan_polygon <- st_polygon(list(azerbaijan_outer_ring, azerbaijan_inner_ring))

azerbaijan_polygon |>
    ggplot() +
    geom_sf(fill = "lightblue") +
    theme_bw()

References

Chambers, John M. 2016. Extending R. CRC Press LLC.
James, Gareth. 2021. An Introduction to Statistical Learning: With Applications in R. Second edition. Springer Texts in Statistics. Springer.
Pebesma, Edzer, and Roger Bivand. 2023. Spatial Data Science: With Applications in R. Chapman and Hall/CRC. https://doi.org/10.1201/9780429459016.

Footnotes

  1. p. 4↩︎

  2. James (2021), p. 43↩︎