Intro to Point Pattern Analysis using R and QGIS
This example shows how to use R and QGIS from within R to perform a series of common point pattern analysis techniques.
library(mapview)
library(sf)
## Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
library(censusxy)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
<-read.csv(url("https://raw.githubusercontent.com/coreysparks/data/master/wic_west_side.csv"))
addr<-addr[c(6, 12:14)]
addrnames(addr)<-c("street", "city", "st", "zip")
head(addr)
## street city st zip
## 1 1039 W Hildebrand Ave San Antonio TX 78201
## 2 1102 Delgado St San Antonio TX 78207
## 3 1115 W Martin St San Antonio TX 78207
## 4 1115 W Martin St San Antonio TX 78207
## 5 1115 W Martin St San Antonio TX 78207
## 6 1115 W Martin St San Antonio TX 78207
<-cxy_geocode(addr,
resultsstreet = "street",
city = "city",
state ="st",
zip = "zip",
class="sf",
output = "simple")
## 33 rows removed to create an sf object. These were addresses that the geocoder could not match.
<-st_transform(results,
results.projcrs = 2278)
OR just use the lat / long information in the data!
<-read.csv(url("https://raw.githubusercontent.com/coreysparks/data/master/wic_west_side.csv"))
addr<- st_as_sf(addr, coords=c("Longitude", "Latitude"), crs=4269,agr="constant")
results <-st_transform(results,
results.projcrs = 2278)
= mapview(results.proj)
mv1mapshot(mv1, file = paste0(getwd(), "/map1.png"))
library(tmap)
library(tmaptools)
library(OpenStreetMap)
<- read_osm(results.proj, ext=1.1)
bg
tmap_mode("plot")
## tmap mode set to plotting
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(results.proj)+
tm_dots()
mean feature - average of coordinates
<-apply(st_coordinates(results.proj), MARGIN = 2, FUN = mean)
mean_feature<-data.frame(place="meanfeature", x=mean_feature[1], y= mean_feature[2])
mean_feature<-st_as_sf(mean_feature, coords = c("x", "y"), crs= 2278)
mean_feature
tmap_mode("view")
## tmap mode set to interactive viewing
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(results.proj, size = .2)+
tm_dots(col = "green")+
tm_shape(mean_feature)+
tm_dots(col = "red", size = .2)
Central feature - Median of coordinates
<-apply(st_coordinates(results.proj), MARGIN = 2, FUN = median)
median_feature<-data.frame(place="medianfeature", x=median_feature[1], y= median_feature[2])
median_feature<-st_as_sf(median_feature, coords = c("x", "y"), crs= 2278)
median_feature
tmap_mode("plot")
## tmap mode set to plotting
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(results.proj)+
tm_dots(col = "green", size = .2)+
tm_shape(mean_feature)+
tm_dots(col = "red", size=.2)+
tm_shape(median_feature)+
tm_dots(col = "blue", size = .2)
Buffer points
<- st_buffer(results.proj, dist = 2500)
wicbuff
tmap_mode("plot")
## tmap mode set to plotting
tm_basemap("OpenStreetMap.Mapnik" )+
tm_shape(results.proj, is.master = T)+
tm_dots(col = "green")+
tm_shape(wicbuff)+
tm_polygons(col = "red", alpha = .1)
Convex hull plot
<- st_convex_hull(st_union(results)) chull
## although coordinates are longitude/latitude, st_union assumes that they are planar
tmap_mode("plot")
## tmap mode set to plotting
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(results.proj)+
tm_dots(col = "green")+
tm_shape(chull)+
tm_polygons(col = "grey", alpha = .5)
kernel density - You need projected data for this to work right
R can do kernel density maps, but using simple features it’s kind of complicated. I will use Qgis through R instead using the qgisprocess
package
library(qgisprocess)
## Using 'qgis_process' at 'C://OSGeo4W64/bin/qgis_process-qgis.bat'.
## Run `qgis_configure()` for details.
qgis_configure()
## getOption('qgisprocess.path') was not found.
## Sys.getenv('R_QGISPROCESS_PATH') was not found.
## Trying 'qgis_process' on PATH
## Error in rethrow_call(c_processx_exec, command, c(command, args), stdin, : Command 'qgis_process' not found @win/processx.c:994 (processx_exec)
## Found 2 QGIS installations containing 'qgis_process':
## C://OSGeo4W64/bin/qgis_process-qgis.bat
## C://OSGeo4W64/bin/qgis_process-qgis-dev.bat
## Trying command 'C://OSGeo4W64/bin/qgis_process-qgis.bat'
## Success!
To use this, we need to find the name of the Qgis algorithm we want. qgis_algorithms()
can return all available algorithms, then we can either filter it with View()
or use grep to search for one.
<-qgis_algorithms()
algsgrepl(pattern = "density", x = algs$algorithm ),] algs[
## # A tibble: 8 x 5
## provider provider_title algorithm algorithm_id algorithm_title
## <chr> <chr> <chr> <chr> <chr>
## 1 grass7 GRASS grass7:r.li.ed~ r.li.edgedensity r.li.edgedensity
## 2 grass7 GRASS grass7:r.li.ed~ r.li.edgedensit~ r.li.edgedensity.as~
## 3 grass7 GRASS grass7:r.li.pa~ r.li.patchdensi~ r.li.patchdensity
## 4 grass7 GRASS grass7:r.li.pa~ r.li.patchdensi~ r.li.patchdensity.a~
## 5 native QGIS (native c~ native:lineden~ linedensity Line density
## 6 qgis QGIS qgis:heatmapke~ heatmapkernelde~ Heatmap (Kernel Den~
## 7 saga SAGA saga:fragmenta~ fragmentationcl~ Fragmentation class~
## 8 saga SAGA saga:kernelden~ kerneldensityes~ Kernel density esti~
qgis_show_help("qgis:heatmapkerneldensityestimation")
## https://coreysparks.github.io/blog
## Heatmap (Kernel Density Estimation) (qgis:heatmapkerneldensityestimation)
##
## ----------------
## Description
## ----------------
##
## ----------------
## Arguments
## ----------------
##
## INPUT: Point layer
## Argument type: source
## Acceptable values:
## - Path to a vector layer
## RADIUS: Radius
## Argument type: distance
## Acceptable values:
## - A numeric value
## RADIUS_FIELD: Radius from field
## Argument type: field
## Acceptable values:
## - The name of an existing field
## - ; delimited list of existing field names
## PIXEL_SIZE: Output raster size
## Argument type: number
## Acceptable values:
## - A numeric value
## WEIGHT_FIELD: Weight from field
## Argument type: field
## Acceptable values:
## - The name of an existing field
## - ; delimited list of existing field names
## KERNEL: Kernel shape
## Argument type: enum
## Available values:
## - 0: Quartic
## - 1: Triangular
## - 2: Uniform
## - 3: Triweight
## - 4: Epanechnikov
## Acceptable values:
## - Number of selected option, e.g. '1'
## - Comma separated list of options, e.g. '1,3'
## DECAY: Decay ratio (Triangular kernels only)
## Argument type: number
## Acceptable values:
## - A numeric value
## OUTPUT_VALUE: Output value scaling
## Argument type: enum
## Available values:
## - 0: Raw
## - 1: Scaled
## Acceptable values:
## - Number of selected option, e.g. '1'
## - Comma separated list of options, e.g. '1,3'
## OUTPUT: Heatmap
## Argument type: rasterDestination
## Acceptable values:
## - Path for new raster layer
##
## ----------------
## Outputs
## ----------------
##
## OUTPUT: <outputRaster>
## Heatmap
Run the algorithm
<-qgis_run_algorithm(algorithm ="qgis:heatmapkerneldensityestimation",
wic_densINPUT=results.proj,
RADIUS = 5000,
PIXEL_SIZE = 100,
KERNEL = 0,
OUTPUT=file.path(getwd(), "wicdenst.TIF"),
load_output = TRUE)
## Running "C://OSGeo4W64/bin/qgis_process-qgis.bat" run \
## "qgis:heatmapkerneldensityestimation" \
## "--INPUT=C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X\file4b6cddc2e7a\file4b6c64dc2e36.gpkg" \
## "--RADIUS=5000" "--PIXEL_SIZE=100" "--KERNEL=0" "--OUTPUT_VALUE=0" \
## "--OUTPUT=https://coreysparks.github.io/blog/wicdenst.TIF"
## https://coreysparks.github.io/blog
##
## ----------------
## Inputs
## ----------------
##
## INPUT: C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X\file4b6cddc2e7a\file4b6c64dc2e36.gpkg
## KERNEL: 0
## OUTPUT: https://coreysparks.github.io/blog/wicdenst.TIF
## OUTPUT_VALUE: 0
## PIXEL_SIZE: 100
## RADIUS: 5000
##
##
## 0...10...20...30...40...50...60...70...80...90...
## ----------------
## Results
## ----------------
##
## OUTPUT: https://coreysparks.github.io/blog/wicdenst.TIF
library(raster)
library(RColorBrewer)
<- qgis_as_raster(wic_dens)
result
projection(result)<-crs(results.proj)
tmap_mode("plot")
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(result)+
tm_raster()+
tm_shape(results.proj, is.master = T)+
tm_dots(col="red")
Spatial join
A spatial join can combine attributes of one layer with another layer. Here I combine census variables with the WIC clinic points.
library(tidycensus)
library(dplyr)
#load census tract data
<-get_acs(geography = "tract",
sa_acsstate="TX",
county = "Bexar",
year = 2019,
variables=c( "DP05_0001E", "DP03_0009P", "DP03_0062E", "DP03_0119PE",
"DP05_0001E","DP02_0009PE","DP02_0008PE","DP02_0040E","DP02_0038E",
"DP02_0066PE","DP02_0067PE","DP02_0080PE","DP02_0092PE",
"DP03_0005PE","DP03_0028PE","DP03_0062E","DP03_0099PE","DP03_0101PE",
"DP03_0119PE","DP04_0046PE","DP05_0072PE","DP05_0073PE",
"DP05_0066PE", "DP05_0072PE", "DP02_0113PE") ,
geometry = T, output = "wide")
## Getting data from the 2015-2019 5-year ACS
## Downloading feature geometry from the Census website. To cache shapefiles for use in future sessions, set `options(tigris_use_cache = TRUE)`.
## Using the ACS Data Profile
## Using the ACS Data Profile
#rename variables and filter missing cases
<-sa_acs%>%
sa_acs2mutate(totpop= DP05_0001E, pwhite=DP05_0072PE,
pblack=DP05_0073PE , phisp=DP05_0066PE,
phsormore=DP02_0066PE,punemp=DP03_0009PE, medhhinc=DP03_0062E,
ppov=DP03_0119PE)%>%
::select(GEOID, totpop, pblack, pwhite, phisp, punemp, medhhinc, ppov)
dplyr
<-st_transform(sa_acs2, crs = 2278)
sa_acs2<-st_cast(sa_acs2, "MULTILINESTRING") sa_trol
<-st_join(results.proj, sa_acs2)
spjoin#head(spjoin)
tmap_mode("plot")
## tmap mode set to plotting
tm_basemap("OpenStreetMap.Mapnik")+
tm_shape(spjoin, is.master = T)+
tm_dots("punemp", size = .1)+
tm_shape(sa_acs2)+
tm_polygons(alpha = .1)
Count points in polygons
Point in polygon operations are actually a spatial intersection (more on this next week!) where we see how many points fall within a given polygon.
$nwic<- lengths(st_intersects(sa_acs2, results.proj))
sa_acs2
tmap_mode("plot")
## tmap mode set to plotting
<-tm_basemap("OpenStreetMap.Mapnik")+
mtm_shape(sa_acs2)+
tm_polygons("nwic")+
tm_shape(spjoin, is.master = T)+
tm_dots( size = .01)
m
Thiessen/Voronoi Polygons
Thiessen or Voronoi polygons are a process where we can convert points into polygons.
grepl(pattern = "voronoi", x = algs$algorithm ),] algs[
## # A tibble: 3 x 5
## provider provider_title algorithm algorithm_id algorithm_title
## <chr> <chr> <chr> <chr> <chr>
## 1 grass7 GRASS grass7:v.voronoi v.voronoi v.voronoi
## 2 grass7 GRASS grass7:v.voronoi.sk~ v.voronoi.skele~ v.voronoi.skele~
## 3 qgis QGIS qgis:voronoipolygons voronoipolygons Voronoi polygons
qgis_show_help("qgis:voronoipolygons")
## https://coreysparks.github.io/blog
## Voronoi polygons (qgis:voronoipolygons)
##
## ----------------
## Description
## ----------------
## This algorithm takes a points layer and generates a polygon layer containing the voronoi polygons corresponding to those input points.
##
##
## ----------------
## Arguments
## ----------------
##
## INPUT: Input layer
## Argument type: source
## Acceptable values:
## - Path to a vector layer
## BUFFER: Buffer region (% of extent)
## Argument type: number
## Acceptable values:
## - A numeric value
## OUTPUT: Voronoi polygons
## Argument type: sink
## Acceptable values:
## - Path for new vector layer
##
## ----------------
## Outputs
## ----------------
##
## OUTPUT: <outputVector>
## Voronoi polygons
<-qgis_run_algorithm(alg="qgis:voronoipolygons",
wic_vonINPUT=results.proj,
OUTPUT=file.path(tempdir(), "wicvon.shp"),
load_output = TRUE)
<-sf::read_sf(qgis_output(wic_von, "OUTPUT"))
wic_von
tmap_mode("view")
<-tm_basemap("OpenStreetMap.Mapnik")+
map1tm_shape(wic_von)+
tm_polygons(col="grey")+
tm_shape(results.proj)+
tm_dots( size = .01)
<- tmap_mode("plot")
current.mode
# plot map
map1
Nearest Neighbor analysis
library(spatstat)
<-as.ppp(as(results.proj, "Spatial"))
wic.pp
plot(nearest.neighbour(wic.pp))
grepl(pattern = "nearest", x = algs$algorithm ),] algs[
## # A tibble: 10 x 5
## provider provider_title algorithm algorithm_id algorithm_title
## <chr> <chr> <chr> <chr> <chr>
## 1 gdal GDAL gdal:gridinver~ gridinversedist~ Grid (IDW with nea~
## 2 gdal GDAL gdal:gridneare~ gridnearestneig~ Grid (Nearest neig~
## 3 native QGIS (native c~ native:angleto~ angletonearest Align points to fe~
## 4 native QGIS (native c~ native:joinbyn~ joinbynearest Join attributes by~
## 5 native QGIS (native c~ native:nearest~ nearestneighbou~ Nearest neighbour ~
## 6 qgis QGIS qgis:distancet~ distancetoneare~ Distance to neares~
## 7 qgis QGIS qgis:distancet~ distancetoneare~ Distance to neares~
## 8 qgis QGIS qgis:knearestc~ knearestconcave~ Concave hull (k-ne~
## 9 saga SAGA saga:knearestn~ knearestneighbo~ K-nearest neighbou~
## 10 saga SAGA saga:nearestne~ nearestneighbour Nearest neighbour
qgis_show_help("native:nearestneighbouranalysis")
## https://coreysparks.github.io/blog
## Nearest neighbour analysis (native:nearestneighbouranalysis)
##
## ----------------
## Description
## ----------------
## This algorithm performs nearest neighbor analysis for a point layer.
##
## The output describes how the data are distributed (clustered, randomly or distributed).
##
## Output is generated as an HTML file with the computed statistical values.
##
## ----------------
## Arguments
## ----------------
##
## INPUT: Input layer
## Argument type: source
## Acceptable values:
## - Path to a vector layer
## OUTPUT_HTML_FILE: Nearest neighbour
## Argument type: fileDestination
## Acceptable values:
## - Path for new file
##
## ----------------
## Outputs
## ----------------
##
## OUTPUT_HTML_FILE: <outputHtml>
## Nearest neighbour
## OBSERVED_MD: <outputNumber>
## Observed mean distance
## EXPECTED_MD: <outputNumber>
## Expected mean distance
## NN_INDEX: <outputNumber>
## Nearest neighbour index
## POINT_COUNT: <outputNumber>
## Number of points
## Z_SCORE: <outputNumber>
## Z-score
<-qgis_run_algorithm(alg="native:nearestneighbouranalysis",
wic_nnINPUT=results.proj,
OUTPUT_HTML_FILE=file.path(tempdir(), "wicnn.html"),
load_output = TRUE)
## Ignoring unknown input 'load_output'
## Running "C://OSGeo4W64/bin/qgis_process-qgis.bat" run \
## "native:nearestneighbouranalysis" \
## "--INPUT=C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X\file4b6cddc2e7a\file4b6c441e236d.gpkg" \
## "--OUTPUT_HTML_FILE=C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X/wicnn.html"
## https://coreysparks.github.io/blog
## The system cannot find the file C:\rtools40\Version.txt.
##
## ----------------
## Inputs
## ----------------
##
## INPUT: C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X\file4b6cddc2e7a\file4b6c441e236d.gpkg
## OUTPUT_HTML_FILE: C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X/wicnn.html
##
##
## 0...10...20...30...40...50...60...70...80...90...100 - done.
##
## ----------------
## Results
## ----------------
##
## EXPECTED_MD: 1356.5835389412343
## NN_INDEX: 0.4987323168352079
## OBSERVED_MD: 676.5720513566673
## OUTPUT_HTML_FILE: C:\Users\ozd504\AppData\Local\Temp\Rtmp2F9m0X/wicnn.html
## POINT_COUNT: 100
## Z_SCORE: -9.589602141964955