Generating territories

R Skill Level: Intermediate - this activity assumes you have a working knowledge of R. If you’re not familiar with the apply family of operations in R you may wish to brush up on those before proceeding. See the help files for more information.

Objectives & Goals

Upon completion of this activity, you will:
  • create territories using kernel density estimates
  • create territories using minimum convex polygon
  • find the area of each territory
  • calculate an index of density
  • Extract raster data to terrtiories



Required packages

To complete the following activity you will need the following packages installed: raster sp rgeos

Installing the packages

If installing these packages for the first time consider adding dependencies=TRUE
  • install.packages("raster",dependencies = TRUE)
  • install.packages("rgdal",dependencies = TRUE)
  • install.packages("rgeos",dependencies = TRUE)
  • install.packages("sp",dependencies = TRUE)


  • Download R script Last modified: 2019-09-20 18:26:28


    If you work with marked populations or have telemetry-like data you may be interested in constructing territories with either a minimum convex polygon or using kernel density estimation. This activity will illustrate how to go from GPS coordinates of locations for individuals to a territories using both minimum convex polygons and kernel density estimates.

    We’ll use location data from a marked population of Ovenbirds breeding at Hubbard Brook Experimental Forest in New Hampshire. The data used in this activity are real data collected in 2012 - we may encounter some hic-cups along the way - we’ll sort them out as/if we encouter them.

    library(raster)
    library(sp)
    library(ks)
    

    Read in bird locations

    The location data are stored as a csv file.

    OVEN_locs <- read.csv("../Spatial_Layers/OVEN_2012_locs.csv")
    
    ## 'data.frame':	1634 obs. of  7 variables:
    ##  $ Type : Factor w/ 1 level "WAYPOINT": 1 1 1 1 1 1 1 1 1 1 ...
    ##  $ ident: Factor w/ 551 levels "1","10","100",..: 13 24 494 51 52 53 54 55 56 58 ...
    ##  $ lat  : num  44 44 44 44 44 ...
    ##  $ long : num  -71.7 -71.7 -71.7 -71.7 -71.7 ...
    ##  $ Date : Factor w/ 55 levels "","1-Jun-12",..: 34 34 48 54 54 54 54 54 54 54 ...
    ##  $ Time : Factor w/ 1566 levels "05:09:30","05:09:36",..: 826 875 112 358 360 372 399 410 428 462 ...
    ##  $ Bird : Factor w/ 64 levels "__AO","A_KK",..: 1 1 1 1 1 1 1 1 1 1 ...
    

    Convert points to SpatialPoints object

    Now that we have the data in R we can create a SpatialPoints object.

    # coords = cbind(long,lat)
    # crs = WGS84 
    OVENpts <- sp::SpatialPoints(coords = cbind(OVEN_locs$long,OVEN_locs$lat),
                                 proj4string = sp::CRS("+init=epsg:4326"))
    
    # take a peek
    head(OVENpts)
    
    ## class       : SpatialPoints 
    ## features    : 1 
    ## extent      : -71.74627, -71.74627, 43.95437, 43.95437  (xmin, xmax, ymin, ymax)
    ## crs         : +init=epsg:4326 +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0
    

    Let’s keep all the data together and make a SpatialPointsDataFrame

    OVEN_spdf <- sp::SpatialPointsDataFrame(OVENpts,OVEN_locs)
    
    head(OVEN_spdf)
    
    ##       Type ident      lat      long      Date     Time Bird
    ## 1 WAYPOINT    11 43.95437 -71.74627 26-May-12 09:13:58 __AO
    ## 2 WAYPOINT    12 43.95435 -71.74624 26-May-12 09:27:42 __AO
    ## 3 WAYPOINT    95 43.95441 -71.74652  6-Jun-12 06:32:15 __AO
    ## 4 WAYPOINT   144 43.95450 -71.74707  9-Jun-12 07:23:48 __AO
    ## 5 WAYPOINT   145 43.95445 -71.74709  9-Jun-12 07:24:17 __AO
    ## 6 WAYPOINT   146 43.95437 -71.74679  9-Jun-12 07:26:39 __AO
    

    One handy function available in R that isn’t in ArcMap (or at least it wasn’t when I stopped using it) is a function to split a shapefile based on attributes to make a series of spatial layers. The split does exactly that. We’ll use it here to make a SpatialPoints object for each individual in the dataset.

    OVEN_sep <- split(x = OVEN_spdf, f = OVEN_spdf$Bird, drop = FALSE)
    

    We now have a list, one element for each unique Bird id. We’ll use this to make our territories.

    Minimum Convex Hull

    Minimum convex polygon (MCP) generates a polygon that encapsulates all points for an individual. The first step is to make a minimum convex polygon for each individual. The gConvexHull does exactly that. Here we’ll using the lapply function to avoid for loops.

    OVENmcp <- lapply(OVEN_sep, FUN = function(x){rgeos::gConvexHull(x)})
    

    The above function returns a list. Ideally, we want all the polygons merged into a single SpatialPolygonsDataFrame. Before we can collapse the list we need to change the ID field in each polygon. Currently, each polygon has the same value in the ID field. Because of that - we can’t combine them. The next bit of code labels each polygon ID field as the Bird id.

    OVENmcp <- mapply(OVENmcp, names(OVENmcp), 
                      SIMPLIFY = FALSE,
                      FUN = function(x,y){x@polygons[[1]]@ID <- y
                      return(x)})
    

    Now that the ID field is unique for each polygon we can merge the polygons together into a single SpatialPolygons object.

    OVENmcp <- do.call(rbind,OVENmcp)
    
    ## class       : SpatialPolygons 
    ## features    : 64 
    ## extent      : -71.756, -71.69537, 43.94234, 43.958  (xmin, xmax, ymin, ymax)
    ## crs         : +init=epsg:4326 +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0
    

    Let’s finish this portion by creating a SpatialPolygonsDataFrame

    OVENmcp <- SpatialPolygonsDataFrame(Sr = OVENmcp,
                                        data = data.frame(Bird = names(OVENmcp)),
                                        match.ID = FALSE)
    
    ## class       : SpatialPolygonsDataFrame 
    ## features    : 64 
    ## extent      : -71.756, -71.69537, 43.94234, 43.958  (xmin, xmax, ymin, ymax)
    ## crs         : +init=epsg:4326 +proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0 
    ## variables   : 1
    ## names       : Bird 
    ## min values  : __AO 
    ## max values  : YWAY
    

    plot of chunk unnamed-chunk-15

    There are some issues. Looks like maybe a few points were labeled incorrectly. We’ll come back to fix those up.

    Back to top

    Kernel density estimation

    Below is just one way to estimate territories using kernel density estimates. We’ll use least-square cross validation to estimate the bandwidth - see Barg et al. 2005 for more information.

    First, we’ll estimate the bandwidth for each bird. To do that we’ll use the least-squares cross validation (LSCV) estimator that is part of the ks package. We’ll use lapply again to avoid for loops. The following code will estimate the bandwidth using the coordinates for each bird. LSCV doesn’t like having duplicated points - it may return a warning. Here, we’ll just ignore those warnings for now.

    bw <- lapply(OVEN_sep, FUN = function(x){ks::Hlscv(x@coords)})
    
    ## Warning in ks::Hlscv(x@coords): Data contain duplicated values: LSCV is not
    ## well-behaved in this case
    
    ## Warning in ks::Hlscv(x@coords): Data contain duplicated values: LSCV is not
    ## well-behaved in this case
    

    Now that we’ve estimated the bandwidth we can generate the territories. We’ll use the kde (kernel density estimate) in the ks package to create the kernel density estimate. Then in the same function call we’ll convert the kernel density estimate into a raster layer that is spatially explicit. To do this we’ll use the mapply function. See here for more information on what mapply does.

    OVEN_kde <-mapply(OVEN_sep,bw,
                      SIMPLIFY = FALSE,
                      FUN = function(x,y){
                       raster(kde(x@coords,h=y))})
    

    Here’s a quick peek at the difference between the MCP and KDE for the same individual. plot of chunk unnamed-chunk-18

    We’re not finished yet with the making the KDE’s. Let’s create a 95% kernel density estimate. To do that we’ll first need to determine what the 95% contour value is. Then, we’ll set all raster values that are less than that value to NA. Once we have the 95% kernel density estimate, we’ll then convert the raster into a polygon so we can find the area, extract landcover data, etc.

    Let’s find the 95% contour. To do this we’ll write a custom function. The function will take the kde and probability as inputs. The below function call getContour will make the 95% KDE for us.

    # This code makes a custom function called getContour. 
    # Inputs:
    #    kde = kernel density estimate
    #    prob = probabily - default is 0.95
    
    getContour <- function(kde, prob = 0.95){
       # set all values 0 to NA
          kde[kde == 0]<-NA
       # create a vector of raster values
          kde_values <- raster::getValues(kde)
       # sort values 
          sortedValues <- sort(kde_values[!is.na(kde_values)],decreasing = TRUE)
       # find cumulative sum up to ith location
          sums <- cumsum(as.numeric(sortedValues))
       # binary response is value in the probabily zone or not
          p <- sum(sums <= prob * sums[length(sums)])
       # Set values in raster to 1 or 0
          kdeprob <- raster::setValues(kde, kde_values >= sortedValues[p])
       # return new kde
          return(kdeprob)
    }
    
    OVEN_95kde <- lapply(OVEN_kde,
                       FUN = getContour,prob = 0.95)
    

    plot of chunk unnamed-chunk-21

    Now that we have the 95% KDE, let’s make a polygon. We’ll use raster’s rasterToPolygons to do that. First, we’ll change all the values in the 95% KDE to 1. After we do that we’ll covert to a polygon. We first convert all values to 1 so that we only make a single polygon that represents the 95% KDE.

    OVEN_95poly <- lapply(OVEN_95kde, 
                          FUN = function(x){
                            x[x==0]<-NA
                            y <- rasterToPolygons(x, dissolve = TRUE)
                            return(y)
                          })
    

    Before we can merge the 95% KDE together into a single SpatialPolygonsDataFrame we need to change the polygon ID field. We did this above in the MCP example. We’ll use the same processes again here.

    OVEN_95poly <- mapply(OVEN_95poly, names(OVEN_95poly), 
                      SIMPLIFY = FALSE,
                      FUN = function(x,y){x@polygons[[1]]@ID <- y
                      return(x)})
    

    Now that the ID field is unique for each polygon we can merge the polygons together into a single SpatialPolygons object.

    OVEN_95poly <- do.call(rbind,OVEN_95poly)
    

    We’ll finish this portion by adding the bird ID field to the SpatialPolygonsDataFrame. I introduce a new helper function within the sp package called getSpPolygonsIDSlots which does exactly as the function suggests - it returns the ID slot within an object of class SpatialPolygon.

    OVEN_95poly$Bird <- getSpPPolygonsIDSlots(OVEN_95poly)
    
    ## Warning: use *apply and slot directly
    

    plot of chunk unnamed-chunk-26

    Back to top

    Calculate attributes / Extract raster data

    Now that we have made some territories - let’s extract some information. One attribute of interest is territory size. Before we calculate the size, let’s take a look at the crs.

    OVEN_95poly
    
    ## class       : SpatialPolygonsDataFrame 
    ## features    : 64 
    ## extent      : -71.75641, -71.69242, 43.9414, 43.95862  (xmin, xmax, ymin, ymax)
    ## crs         : +proj=longlat +datum=WGS84 +ellps=WGS84 +towgs84=0,0,0 
    ## variables   : 2
    ## names       : layer, Bird 
    ## min values  :     1, __AO 
    ## max values  :     1, YWAY
    

    Notice that the crs is WGS84 or in other words - lat/long, not projected. Let’s first project into a projection that will give us meaningful area estimates. Since these data were gathered at Hubbard Brook Experimental Forest in New Hampshire, let’s use UTM, ZONE 19 N (EPSG:32619) as our projection.

    OVENkde_utm <- sp::spTransform(OVEN_95poly, sp::CRS("+init=epsg:32619"))
    

    Now that we have projected data that is in meters we can get a more meaningful measure of area. We’ll use the gArea function in the rgeos package to calculate the area. Let’s have it return the area in hectares.

    rgeos::gArea(OVENkde_utm, byid = TRUE)/10000
    
    ##       __AO       A_KK       A_MM       APRY       BBAR       BBAW 
    ##  1.0673764  2.4870911  1.8667872  5.0575723  1.9110031  1.0059919 
    ##       BOGA       BRAO       BRAW       BRGA       BWAO       BYAK 
    ##  2.9517315  2.1417197 20.3285926  0.6728364  2.7020183  1.7512344 
    ##       BYWA       GGAR       GRAO       GRAW       GRGA       GWAG 
    ##  2.7786690  5.6030768  3.7193737  1.0639960 14.5445779  4.6073879 
    ##       GYAB       GYAO       KRAG       KROA       KWAG       KYAR 
    ## 35.7228287  1.1734981  1.8457727  4.1336069  0.8192061  9.5846092 
    ##       KYBA       MBAY       MBWA       MM_A       MMAY       MWGA 
    ##  6.2356445  2.1787982  2.8555932  2.5728915  3.6637620  5.0746542 
    ##       OBAK       OKBA       OOYA       ORWA       OWAK   OWOA_MID 
    ##  4.9705965  3.4475742  2.6901538  5.3687464 13.5747309  1.4972174 
    ##       OYAR       PKYA       PPAK       PWKA       PYYA       RBAB 
    ##  0.8822659  4.6840713  5.2457076  3.6238212  1.4511408  0.6944329 
    ##       RGAW       RKAR       ROGA       RWAP       RWKA       RYWA 
    ##  2.5649554 39.4765430 14.8932943  4.5950448  2.2485388  2.6918551 
    ##       WA_Y       WBOA       WGAY       WGGA       WOAW       WOGA 
    ##  4.6116720  1.5454525 21.9530011  2.9143715  1.9042253  2.3967992 
    ##       WORA       WPAB       WWAB       WWAO       YAOY       YBWA 
    ##  2.3690225  5.2167178  3.0969429  3.2959241  6.7857750  4.7502034 
    ##       YKAG       YOAB       YOKA       YWAY 
    ##  2.4014232  1.6383173  2.9426634  3.8435588
    

    Let’s extract some environmental data to our territories. Here, we’ll extract elevation but the process is the same regardless of the variable so long as it’s a raster.

    First, we need to read in the digital elevation model (DEM).

    # read in raster
    DEM <- raster::raster("../Spatial_Layers/hb10mdem.txt")
    
    # project raster to match territories
    DEMutm <- projectRaster(DEM, crs = "+init=epsg:32619")
    

    plot of chunk unnamed-chunk-31

    Extract elevation values underlying the territories and append the values to the SpatialPolygonsDataFrame.

    OVENkde_utm$meanElev <- extract(DEMutm,OVENkde_utm, mean)
    
    ##      BirdID  area_ha Elev_mean Elev_lower Elev_upper
    ## __AO   __AO 1.067376  742.0644   727.1407   755.8415
    ## A_KK   A_KK 2.487091  629.2352   617.2643   640.8459
    ## A_MM   A_MM 1.866787  581.0430   557.9654   591.5341
    ## APRY   APRY 5.057572  543.4946   523.2714   570.2570
    ## BBAR   BBAR 1.911003  741.2363   724.1396   777.4179
    ## BBAW   BBAW 1.005992  780.5575   732.5729   801.6216
    

    back to top

    Calculate local density metrics

    We’ll just scratch the surface here but we’ll compute a basic density metric using the territories we just created. In order to do that we’ll need a raster surface of the area. We can create a basic raster layer using the raster function. Let’s set the res to something spatially meaningful. Below we set it to 100x100m or 1 hectare.

    # Empty raster with 1ha resolution
    study_plot_raster <- raster::raster(OVENkde_utm, res =c(100,100))
    
    # Covert to SpatialPolygon
    study_grid <- as(study_plot_raster,"SpatialPolygons")
    

    plot of chunk unnamed-chunk-35

    For our simple ‘density’ metric let’s figure out how many unique birds use each one hectare grid within the system.

    # Create a list with all birds wihtin each 1ha grid cell.
    birds_in_ha <- sp::over(study_grid, OVENkde_utm, returnList = TRUE)
    
    # Find how many unique birds are in each cell 
    study_grid$birds <- unlist(lapply(birds_in_ha,FUN=function(x){length(unique(x$Bird))}))
    
    #convert to raster 
    birds.per.ha <- rasterize(study_grid,study_plot_raster,study_grid$birds)
    

    plot of chunk unnamed-chunk-37

    The figure above is a bit misleading. It’s unlikely that there are no Ovenbirds between our study areas.

    Challenge:

    Take a minute to think about how you may go about removing the areas between our study plots (where we didn't follow individuals). There is more than one way to complete this task. How could you use Spatial Predicates, extract or over to determine which 1 hectare grids were sampled. Perhaps dissolving (gUnion) may be useful.


    back to top