GESIS Workshop: Introduction to Geospatial Techniques for Social Scientists in R
Stefan Jünger, Anne-Kathrin Stroppe, Dennis Abel
2026-04-23
Now
Day
Time
Title
April 09
10:00-11:30
Introduction
April 09
11:30-11:45
Coffee Break
April 09
11:45-13:00
Data Formats
April 09
13:00-14:00
Lunch Break
April 09
14:00-15:30
Mapping
April 09
15:30-15:45
Coffee Break
April 09
15:45-17:00
Spatial Wrangling
April 10
09:00-10:30
Spatial Wrangling
April 10
10:30-10:45
Coffee Break
April 10
10:45-12:00
Applied Spatial Linking
April 10
12:00-13:00
Lunch Break
April 10
13:00-14:30
Spatial Analysis
April 10
14:30-14:45
Coffee Break
April 10
14:45-16:00
Spatial Econometrics & Outlook
Mini wrap-up
Thus far, we have learned about
Different data formats
How to load them
First steps in interacting with them
Creating maps with ggplot2
But the real magic in geospatial data lies in their flexibility.
Our plan for this afternoon
In this session, you will learn
How to wrangle the different geospatial data formats even further
Link/join different datasets
1. Advanced Importing
Importing of non-spatial data
Say the data we want to use are not available as a shapefile. Point coordinates are often stored in table formats like .csv – like the location of charging stations for electric cars data in our ./data folder.
# Loading a simple CSV fileecharging_df <- readr::read_delim("./data/charging_points_ger.csv", delim =";")echarging_df
# A tibble: 60,560 × 7
operator federal_state latitude longitude power_kw type num_plugs
<chr> <chr> <dbl> <dbl> <dbl> <chr> <dbl>
1 deer GmbH Baden-Württemberg 48.3 9.72 44 Normalladeeinrichtung 2
2 EnBW mobility+ AG und Co.KG Baden-Württemberg 48.6 9.87 93 Schnellladeeinrichtung 2
3 SWU Energie GmbH Baden-Württemberg 48.5 10.2 44 Normalladeeinrichtung 2
4 SWU Energie GmbH Baden-Württemberg 48.6 10.1 44 Normalladeeinrichtung 2
5 SWU Energie GmbH Baden-Württemberg 48.2 10.1 22 Normalladeeinrichtung 1
6 EnBW mobility+ AG und Co.KG Baden-Württemberg 48.5 9.98 30 Normalladeeinrichtung 2
7 SWU Energie GmbH Baden-Württemberg 48.5 9.99 22 Normalladeeinrichtung 2
8 Albwerk GmbH & Co. KG Baden-Württemberg 48.5 9.76 22 Normalladeeinrichtung 2
9 EnBW ODR AG Baden-Württemberg 48.5 10.0 22 Normalladeeinrichtung 1
10 Autohaus Burger GmbH & Co. KG Baden-Württemberg 48.4 9.77 22 Normalladeeinrichtung 2
# ℹ 60,550 more rows
From data table to geospatial data
We see that besides our attributes (e.g., operator, power,…), the table contains the two variables “longitude” (X) and “latitude” (Y), our point coordinates. When using the command sf::st_as_sf(), it is easy to transform the table into a point layer.
# Transform to spatial data frameecharging_sf <- echarging_df |># there are some missings in the data which is not allowed dplyr::filter(!is.na(longitude) &!is.na(latitude)) |> sf::st_as_sf(coords =c("longitude", "latitude"))# Check the class of the datasetclass(echarging_sf)
[1] "sf" "tbl_df" "tbl" "data.frame"
Final check via plotting
plot(echarging_sf)
Check the CRS!
Make sure to use the option crs = [EPSG_ID]. If not used, your CRS will not be defined, and you can’t perform further commands depending on the CRS. Here, we tried EPSG IO or http://projfinder.com/ to find the correct CRS
echarging_sf <- echarging_df |># there are some missings in the data which is not allowed dplyr::filter(!is.na(longitude) &!is.na(latitude)) |> sf::st_as_sf( coords =c("longitude", "latitude"),crs =4326 )plot(echarging_sf["operator"])
… and the other way round
Do you want to go back to handling a simple data frame? You can quickly achieve this by dropping the geometry column.
# Remove geometry columnecharging_df <- sf::st_drop_geometry(echarging_sf)# Check the outputhead(echarging_df, 2)
# A tibble: 2 × 5
operator federal_state power_kw type num_plugs
<chr> <chr> <dbl> <chr> <dbl>
1 deer GmbH Baden-Württemberg 44 Normalladeeinrichtung 2
2 EnBW mobility+ AG und Co.KG Baden-Württemberg 93 Schnellladeeinrichtung 2
APIs / data from the internet
Geospatial data tend to be quite big, and there’s a pressure to distribute data efficiently. Data dumps (on the internet) may not be helpful
When resources are low
Time’s a factor
The data have a large geographic extent
Instead, a Programming Application Interface (API) is often used.
Say, we’re interested in the accessibility of public transport in Cologne.
Bus, tram, etc.
All platforms and vehicles should be wheel-chair accessible
We can gather this information using OpenStreetMap!
Accessing OSM data: the Overpass API
The Overpass API (formerly known as OSM Server Side Scripting, or OSM3S before 2011) is a read-only API that serves up custom selected parts of the OSM map data. It acts as a database over the web: the client sends a query to the API and returns the data set that corresponds to the query.
It is not only vector data that can be processed through these mechanisms.
The idea is the same for raster data.
Accessing the information through URLs
Just the downloaded data formats differ
One example is data from European Earth observation (EOD) program “Copernicus” and R packages that can be used to download them, such as ecmwfr.
📺Commercial Break📺:
We use ecmwfr for our interface to EOD linking in our R package gxc
Exercise 4_1: OSM Data💪
🖱[Click here for the exercise](https://stefanjuenger.github.io/gesis-workshop-geospatial-techniques-R-2026/exercises/4_1_OSM_Data.html)
2. Linking
‘Spreadsheet join’ 📅 + 📅 = 💖
While much of our previous data were points, we only had a column for the German federal states as administrative information so far. We’re now moving “a layer down” and looking at Germany on a more fine-grained spatial level: the district by joining data like simple spreadsheets.
‘Spreadsheet join’ 📅 + 📅 = 💖
# Load district shapefilegerman_districts <- sf::read_sf("./data/VG250_KRS.shp")german_districts
Simple feature collection with 431 features and 23 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: 280371.1 ymin: 5235856 xmax: 921292.4 ymax: 6101487
Projected CRS: ETRS89 / UTM zone 32N
# A tibble: 431 × 24
ADE GF BSG ARS AGS SDV_ARS GEN BEZ IBZ BEM NBD SN_L SN_R SN_K SN_V1 SN_V2 SN_G FK_S3 NUTS
<int> <int> <int> <chr> <chr> <chr> <chr> <chr> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 4 4 1 01001 01001 01001000… Flen… Krei… 40 -- ja 01 0 01 00 00 000 R DEF01
2 4 4 1 01002 01002 01002000… Kiel Krei… 40 -- ja 01 0 02 00 00 000 R DEF02
3 4 4 1 01003 01003 01003000… Lübe… Krei… 40 -- ja 01 0 03 00 00 000 R DEF03
4 4 4 1 01004 01004 01004000… Neum… Krei… 40 -- ja 01 0 04 00 00 000 R DEF04
5 4 4 1 01051 01051 01051004… Dith… Kreis 42 -- ja 01 0 51 00 00 000 R DEF05
6 4 4 1 01053 01053 01053010… Herz… Kreis 42 -- ja 01 0 53 00 00 000 R DEF06
7 4 4 1 01054 01054 01054005… Nord… Kreis 42 -- ja 01 0 54 00 00 000 R DEF07
8 4 4 1 01055 01055 01055001… Osth… Kreis 42 -- ja 01 0 55 00 00 000 R DEF08
9 4 4 1 01056 01056 01056003… Pinn… Kreis 42 -- ja 01 0 56 00 00 000 R DEF09
10 4 4 1 01057 01057 01057005… Plön Kreis 42 -- ja 01 0 57 00 00 000 R DEF0A
# ℹ 421 more rows
# ℹ 5 more variables: ARS_0 <chr>, AGS_0 <chr>, WSK <date>, DEBKG_ID <chr>, geometry <MULTIPOLYGON [m]>
‘Spreadsheet join’ 📅 + 📅 = 💖
# Load district shapefilegerman_districts <- sf::read_sf("./data/VG250_KRS.shp")german_districts# Load district attributesattributes_districts <- readr::read_csv2("./data/attributes_districts.csv") attributes_districts
# Load district shapefilegerman_districts <- sf::read_sf("./data/VG250_KRS.shp")german_districts# Load district attributesattributes_districts <- readr::read_csv2("./data/attributes_districts.csv") attributes_districts# Join datagerman_districts_enhanced <- german_districts |> dplyr::left_join(attributes_districts, by ="AGS")german_districts_enhanced
Simple feature collection with 431 features and 29 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: 280371.1 ymin: 5235856 xmax: 921292.4 ymax: 6101487
Projected CRS: ETRS89 / UTM zone 32N
# A tibble: 431 × 30
ADE GF BSG ARS AGS SDV_ARS GEN BEZ IBZ BEM NBD SN_L SN_R SN_K SN_V1 SN_V2 SN_G FK_S3 NUTS
<int> <int> <int> <chr> <chr> <chr> <chr> <chr> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 4 4 1 01001 01001 01001000… Flen… Krei… 40 -- ja 01 0 01 00 00 000 R DEF01
2 4 4 1 01002 01002 01002000… Kiel Krei… 40 -- ja 01 0 02 00 00 000 R DEF02
3 4 4 1 01003 01003 01003000… Lübe… Krei… 40 -- ja 01 0 03 00 00 000 R DEF03
4 4 4 1 01004 01004 01004000… Neum… Krei… 40 -- ja 01 0 04 00 00 000 R DEF04
5 4 4 1 01051 01051 01051004… Dith… Kreis 42 -- ja 01 0 51 00 00 000 R DEF05
6 4 4 1 01053 01053 01053010… Herz… Kreis 42 -- ja 01 0 53 00 00 000 R DEF06
7 4 4 1 01054 01054 01054005… Nord… Kreis 42 -- ja 01 0 54 00 00 000 R DEF07
8 4 4 1 01055 01055 01055001… Osth… Kreis 42 -- ja 01 0 55 00 00 000 R DEF08
9 4 4 1 01056 01056 01056003… Pinn… Kreis 42 -- ja 01 0 56 00 00 000 R DEF09
10 4 4 1 01057 01057 01057005… Plön Kreis 42 -- ja 01 0 57 00 00 000 R DEF0A
# ℹ 421 more rows
# ℹ 11 more variables: ARS_0 <chr>, AGS_0 <chr>, WSK <date>, DEBKG_ID <chr>, geometry <MULTIPOLYGON [m]>,
# car_density <dbl>, ecar_share <chr>, publictransport_meandist <dbl>, population <dbl>, green_voteshare <dbl>,
# afd_voteshare <dbl>
Spatial join
But what can we do if we do not have a matching identifier? For example, there are no matching administrative identifiers in the German district data and the e-charger data.
german_districts_enhanced
Simple feature collection with 431 features and 29 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: 280371.1 ymin: 5235856 xmax: 921292.4 ymax: 6101487
Projected CRS: ETRS89 / UTM zone 32N
# A tibble: 431 × 30
ADE GF BSG ARS AGS SDV_ARS GEN BEZ IBZ BEM NBD SN_L SN_R SN_K SN_V1 SN_V2 SN_G FK_S3 NUTS
<int> <int> <int> <chr> <chr> <chr> <chr> <chr> <int> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 4 4 1 01001 01001 01001000… Flen… Krei… 40 -- ja 01 0 01 00 00 000 R DEF01
2 4 4 1 01002 01002 01002000… Kiel Krei… 40 -- ja 01 0 02 00 00 000 R DEF02
3 4 4 1 01003 01003 01003000… Lübe… Krei… 40 -- ja 01 0 03 00 00 000 R DEF03
4 4 4 1 01004 01004 01004000… Neum… Krei… 40 -- ja 01 0 04 00 00 000 R DEF04
5 4 4 1 01051 01051 01051004… Dith… Kreis 42 -- ja 01 0 51 00 00 000 R DEF05
6 4 4 1 01053 01053 01053010… Herz… Kreis 42 -- ja 01 0 53 00 00 000 R DEF06
7 4 4 1 01054 01054 01054005… Nord… Kreis 42 -- ja 01 0 54 00 00 000 R DEF07
8 4 4 1 01055 01055 01055001… Osth… Kreis 42 -- ja 01 0 55 00 00 000 R DEF08
9 4 4 1 01056 01056 01056003… Pinn… Kreis 42 -- ja 01 0 56 00 00 000 R DEF09
10 4 4 1 01057 01057 01057005… Plön Kreis 42 -- ja 01 0 57 00 00 000 R DEF0A
# ℹ 421 more rows
# ℹ 11 more variables: ARS_0 <chr>, AGS_0 <chr>, WSK <date>, DEBKG_ID <chr>, geometry <MULTIPOLYGON [m]>,
# car_density <dbl>, ecar_share <chr>, publictransport_meandist <dbl>, population <dbl>, green_voteshare <dbl>,
# afd_voteshare <dbl>
Spatial join
But what can we do if we do not have a matching identifier? For example, there are no matching administrative identifiers in the German district data and the e-charger data.
german_districts_enhancedecharging_sf
Simple feature collection with 60549 features and 5 fields
Geometry type: POINT
Dimension: XY
Bounding box: xmin: 5.243745 ymin: 47.2844 xmax: 15.54381 ymax: 55.50014
Geodetic CRS: WGS 84
# A tibble: 60,549 × 6
operator federal_state power_kw type num_plugs geometry
* <chr> <chr> <dbl> <chr> <dbl> <POINT [°]>
1 deer GmbH Baden-Württemberg 44 Normalladeeinrichtung 2 (9.72289 48.32789)
2 EnBW mobility+ AG und Co.KG Baden-Württemberg 93 Schnellladeeinrichtung 2 (9.87484 48.57853)
3 SWU Energie GmbH Baden-Württemberg 44 Normalladeeinrichtung 2 (10.1934 48.52898)
4 SWU Energie GmbH Baden-Württemberg 44 Normalladeeinrichtung 2 (10.08268 48.55354)
5 SWU Energie GmbH Baden-Württemberg 22 Normalladeeinrichtung 1 (10.07698 48.17996)
6 EnBW mobility+ AG und Co.KG Baden-Württemberg 30 Normalladeeinrichtung 2 (9.980588 48.48039)
7 SWU Energie GmbH Baden-Württemberg 22 Normalladeeinrichtung 2 (9.985855 48.48316)
8 Albwerk GmbH & Co. KG Baden-Württemberg 22 Normalladeeinrichtung 2 (9.760458 48.46448)
9 EnBW ODR AG Baden-Württemberg 22 Normalladeeinrichtung 1 (10.02912 48.49882)
10 Autohaus Burger GmbH & Co. KG Baden-Württemberg 22 Normalladeeinrichtung 2 (9.774676 48.40366)
# ℹ 60,539 more rows
💡Solution: We conduct a spatial join!
Adding district information to the point layer
# Harmonize CRS between two datasets echarging_sf_transformed <- sf::st_crs(german_districts) |> sf::st_transform( echarging_sf, crs = _ )
Adding district information to the point layer
# Harmonize CRS between two datasets echarging_sf_transformed <- sf::st_crs(german_districts) |> sf::st_transform( echarging_sf, crs = _ )# We are only interested in the municipality identifier# AGS in the districts dataset; let's create a subsetgerman_districts_subset <- dplyr::select(german_districts, AGS)german_districts_subset
# Harmonize CRS between two datasets echarging_sf_transformed <- sf::st_crs(german_districts) |> sf::st_transform( echarging_sf, crs = _ )# We are only interested in the municipality identifier# AGS in the districts dataset; let's create a subsetgerman_districts_subset <- dplyr::select(german_districts, AGS)german_districts_subset# Finally, joining the two datasets is easyecharging_sf_joined <- sf::st_join( echarging_sf_transformed, german_districts_subset )plot(echarging_sf_joined["AGS"])
Counting objects
Imagine we want to count the number of charging stations in each German district.
Imagine we want to count the number of charging stations in each German district. We now use sf::st_intersects() to identify all intersecting charging stations in each district.
# Again, adjust crs firstecharging_sf_transformed <- sf::st_transform( echarging_sf, crs = sf::st_crs(german_districts) )# Identify all chargers in each districtchargers_in_districts <- sf::st_intersects(german_districts_enhanced, echarging_sf_transformed)chargers_in_districts
Imagine we want to count the number of charging stations in each German district. We now use sf::st_intersects() to identify all intersecting charging stations in each district.
# Again, adjust crs firstecharging_sf_transformed <- sf::st_transform( echarging_sf, crs = sf::st_crs(german_districts) )# Identify all chargers in each districtchargers_in_districts <- sf::st_intersects(german_districts_enhanced, echarging_sf_transformed)chargers_in_districts# Finally counting them and adding them as a variablegerman_districts_enhanced$charger_in_districts <-lengths(chargers_in_districts)german_districts_enhanced$charger_in_districts
One might be interested in only one specific area of Germany, like Cologne. To subset a sf object, you can often use your usual data wrangling workflow. In this case, I know the municipality ID (AGS), which is the only row (and column) I want to keep.
If you have no information about IDs but only about the geolocation, you can use methods like sf::st_touches() (or again sf::st_intersects(), or sf::st_within(), sf::st_crosses(), …) to identify, for example, all districts which share a border with Cologne.
# Extract the 'touch points' between two datasetscologne_touch_points <- sf::st_touches(german_districts_enhanced, cologne)cologne_touch_points
Sparse geometry binary predicate list of length 431, where the predicate was `touches'
first 10 elements:
1: (empty)
2: (empty)
3: (empty)
4: (empty)
5: (empty)
6: (empty)
7: (empty)
8: (empty)
9: (empty)
10: (empty)
Using sf for subsetting
If you have no information about IDs but only about the geolocation, you can use methods like sf::st_touches() (or again sf::st_intersects(), or sf::st_within(), sf::st_crosses(), …) to identify, for example, all districts which share a border with Cologne.
# Extract the 'touch points' between two datasetscologne_touch_points <- sf::st_touches(german_districts_enhanced, cologne)# Again, we are counting the numbers of touch points. cologne_touch_points <-lengths(cologne_touch_points) cologne_touch_points
If you have no information about IDs but only about the geolocation, you can use methods like sf::st_touches() (or again sf::st_intersects(), or sf::st_within(), sf::st_crosses(), …) to identify, for example, all districts which share a border with Cologne.
# Extract the 'touch points' between two datasetscologne_touch_points <- sf::st_touches(german_districts_enhanced, cologne)# Again, we are counting the numbers of touch points. cologne_touch_points <-lengths(cologne_touch_points) cologne_touch_points# Length of mutual border > 0cologne_touch_points <-lengths(cologne_touch_points) >0cologne_touch_points
If you have no information about IDs but only about the geolocation, you can use methods like sf::st_touches() (or again sf::st_intersects(), or sf::st_within(), sf::st_crosses(), …) to identify, for example, all districts which share a border with Cologne.
# extract the 'touch points' between two datasetscologne_touch_points <- sf::st_touches(german_districts_enhanced, cologne)# again, we are counting the numbers of touch points. cologne_touch_points <-lengths(cologne_touch_points) cologne_touch_points# length of mutual border > 0cologne_touch_points <- cologne_touch_points >0cologne_touch_pointscologne_surrounding <- german_districts_enhanced |> dplyr::select(AGS) |> dplyr::filter(cologne_touch_points) plot(cologne_surrounding)
Cropping data
We could also use as simple spatial extent, e.g., from a raster dataset, to subset our e-charging data. But first, we have to adjust the CRS again (use sf::st_crs(echarging_sf) to look up EPSG code):
Now we can use a procedure called cropping to subset the data:
echarging_sf_cologne <- echarging_sf |># if possible, always transform to CRS of the raster dataset sf::st_transform(3035) |> sf::st_crop(inhabitants_cologne)
E-charging stations in Cologne
The result is an vector dataset comprising e-charging stations in Cologne.
tm_shape(echarging_sf_cologne) +tm_dots()
‘Subsetting’ Raster Layers
As you know, we can subset vector data by simply filtering for specific attribute values. For example, to subset Cologne’s districts only by the one of Deutz, we can use the dplyr for sf data:
# raster data for Cologneinhabitants_cologne <- terra::rast("./data/inhabitants_cologne.tif")inhabitants_cologne[inhabitants_cologne <0] <-NA# Cologne district datadeutz <- sf::st_read("./data/cologne.shp",quiet =TRUE ) |># convert to same CRS as raster data sf::st_transform(3035) |> dplyr::filter(NAME =="Deutz")tm_shape(inhabitants_cologne) +tm_raster() +tm_shape(deutz) +tm_borders()
Cropping raster data
Cropping is a method of cutting out a specific slice of a raster layer based on an input dataset or geospatial extent, such as a bounding box.
If we only want to add one attribute from a vector dataset Y to another vector dataset X, we can conduct a spatial join using sf::st_join() as before. There is nothing new to tell. In the raster data world, these operations are called raster extractions.
Extracting information from raster data
Raster data are helpful when we aim to
Apply calculations that are the same for all geometries in the dataset
Extract information from the raster fast and efficient
To extract the raster values at a specific point by location, also called lookup, we use the following:
immigrant_rate_lookup <- terra::extract(x = immigrant_rate, # raster layer to extract fromy = echarging_sf_cologne, # points to extract toraw =TRUE, # output as raw data (no data.frame)ID =FALSE# no ID column before values (really just the raw data...) )# there are many missing values which is why we are looking at such a weird# data rangeimmigrant_rate_lookup[170:180,]
This information can be added to an existing dataset (our points in this example):
# Adding the extracted values to our dataset as a new column# Note: the output of terra::extract() is a raw data matrix, which produces# not so pretty column names. Therefore, we wrap it in the as.vector()# function.echarging_sf_cologne$immigrant_rate_value <-as.vector(immigrant_rate_lookup)echarging_sf_cologne[170:180,]
Sometimes, extracting information 1:1 is not enough. - It’s too narrow - There is missing information about the surroundings of a point
For this endeavor, we can use the function sf::st_buffer(). Here’s an example of creating a circular area of 500 meters around each point in our e-charging subset data of Cologne:
# original datasf::st_geometry_type( echarging_sf_cologne, by_geometry =FALSE)
[1] POINT
18 Levels: GEOMETRY POINT LINESTRING POLYGON MULTIPOINT MULTILINESTRING MULTIPOLYGON ... TRIANGLE
This information can be added to an existing dataset (our points in this example):
# Adding the extracted values to our dataset as a new column# Note: the output of terra::extract() is a bit annoying as it is stored as a# list (despite the raw = TRUE argument). Therefore, we wrap the output in the# unlist() functionecharging_sf_cologne$immigrant_rate_500 <-unlist(immigrant_rate_buffer)echarging_sf_cologne[170:180,]
We can use the same procedure to aggregate a raster dataset into a vector polygon dataset. That’s a widespread use case. Let’s load our Cologne shapefile.
cologne <- sf::st_read("./data/cologne.shp",quiet =TRUE ) |># transform to CRS of raster data sf::st_transform(3035)cologne
cologne$immigrant_rate <- terra::extract(x = immigrant_rate, y = cologne, fun = mean, na.rm =TRUE, ID =FALSE ) |>unlist()plot(cologne["immigrant_rate"])
Other forms of data aggregation
If you ‘simply’ want to aggregate attributes and geometries of vector data, you can rely on st_combine(x) , st_union(x,y) and st_intersection(x, y) to combine vector datasets, resolve borders and return the intersection of two vector datasets
For raster data, you can aggregate with the function terra::aggregate()(if you have matching raster files) in combination with terra::resample() (if your raster files don’t match).
raster_target_layer <- terra::ext(raster_now_points) |> terra::rast(res =100)points_now_raster <- raster_now_points |> terra::rasterize( raster_target_layer, field ="cat_0", # get it with names(raster_now_points) fun ="mean",background =0 )plot(points_now_raster)
Points of interest data are nice for analyzing urban infrastructure. Let’s draw a quick ‘heatmap’ using observation densities.
echarging_sf_cologne_raster <- terra::rast( echarging_sf_cologne, res =1000 )echarging_sf_cologne_densities <- echarging_sf_cologne |> terra::rasterize( echarging_sf_cologne_raster, fun = length, background =0 )plot(echarging_sf_cologne_densities)
Focal statistics / spatial filter
Focal statistics are another method of including information near a point in space. However, it’s applied to the whole dataset and is independent of arbitrary points we project onto a map.
Relates focal cells to surrounding cells
Vastly used in image processing
But also applicable in social science research, as we will see