Getting started with amt

Johannes Signer

2023-03-28

Basics

Creating a track

The basic building blocks of amt are tracks. Tracks are tibbles with at least two columns that contain the coordinates: x_ and y_. A track behaves exactly like a tibble (the only difference being that we added an other S3 class). Below is an example of creating a track with some dummy locations.

library(dplyr)
library(ggplot2)
library(amt)
df1 <- tibble(x = 1:3, y = 1:3)
is.data.frame(df1)
## [1] TRUE
df1
## # A tibble: 3 × 2
##       x     y
##   <int> <int>
## 1     1     1
## 2     2     2
## 3     3     3
# Now we can create a track
tr1 <- make_track(df1, x, y)
is.data.frame(tr1)
## [1] TRUE
tr1
## # A tibble: 3 × 2
##      x_    y_
## * <int> <int>
## 1     1     1
## 2     2     2
## 3     3     3

At the moment amt supports two types of tracks:

If a track_xy or track_xyt is created with the function make_track, is determined whether or not a timestamp is passed as a third argument (called .t) to the function make_track. In the previous example we only passed x and y coordinates. Hence a track_xy was created.

class(tr1)
## [1] "track_xy"   "tbl_df"     "tbl"        "data.frame"

To create a track_xyt we could do the following

df1 <- tibble(x = 1:3, y = 1:3, t = lubridate::ymd("2017-01-01") + lubridate::days(0:2))
tr2 <- make_track(df1, x, y, t)
class(tr2)
## [1] "track_xyt"  "track_xy"   "tbl_df"     "tbl"        "data.frame"

From the output above we see that a track_xyt is also a track_xy. This means that all methods for track_xy also work for a track_xyt (but not the reverse).

Adding additional information

We can also add additional information for each relocation (e.g., the id of the animal, or some other sensor information such as the DOP). Any number of additional named columns can be passed to make_track. By named we mean, that columns should always be passed in the form of column_name = content to avoid confusion with coordinates and time stamp. We will extend the dummy example from above, by passing 2 more columns (the id of animal and the age).

df1 <- tibble(x = 1:3, y = 1:3, t = lubridate::ymd("2017-01-01") + lubridate::days(0:2), 
                  id = 1, age = 4)

# first we only create a track_xy
tr3 <- make_track(df1, x, y, id = id, age = age)
tr3
## # A tibble: 3 × 4
##      x_    y_    id   age
## * <int> <int> <dbl> <dbl>
## 1     1     1     1     4
## 2     2     2     1     4
## 3     3     3     1     4
# now lets create a track_xyt
tr4 <- make_track(df1, x, y, t, id = id, age = age)
tr4
## # A tibble: 3 × 5
##      x_    y_ t_            id   age
## * <int> <int> <date>     <dbl> <dbl>
## 1     1     1 2017-01-01     1     4
## 2     2     2 2017-01-02     1     4
## 3     3     3 2017-01-03     1     4

Coordinate reference system

make_track has one further optional argument (crs), which allows the user to set a coordinate reference system (CRS) of the track. The CRS needs to be provided as valid EPSG code.

An example with one real animal

In the amt relocation data of one red deer from northern Germany is included. We will use this data set to to illustrate how to create a track.

We begin with loading and inspecting the data.

data(sh)
head(sh)
##   x_epsg31467 y_epsg31467        day     time
## 1     3558403     5999400 2009-02-13 00:02:23
## 2     3558548     5999099 2009-02-13 06:02:21
## 3     3558541     5999019 2009-02-13 12:01:51
## 4     3558453     5999026 2009-02-13 18:00:55
## 5     3558566     5999365 2009-02-14 00:01:36
## 6     3557836     5999185 2009-02-14 06:02:24

Before creating a track, we have to do some data cleaning:

  1. check if any coordinates are missing (and if so, remove the relocation),
  2. parse the date and time,
  3. create a time stamp,
  4. check for duplicated time stamps, and
  5. create two new columns for the id and month of the year.
# check if all observations are complete
all(complete.cases(sh)) # no action required
## [1] TRUE
# parse date and time and create time stamps
sh$ts <- as.POSIXct(lubridate::ymd(sh$day) +
                      lubridate::hms(sh$time))

# check for duplicated time stamps
any(duplicated(sh$ts))
## [1] TRUE
# We have some duplicated time stamps, these need to be removed prior to
# creating a track.
sh <- sh[!duplicated(sh$ts), ]

# create new columns
sh$id <- "Animal 1"
sh$month <- lubridate::month(sh$ts)

Now we can create a track.

tr1 <- make_track(sh, x_epsg31467, y_epsg31467, ts, id = id, month = month)

The column names of the data set already indicate the CRS of the data. We can add this information when creating a track.

tr1 <- make_track(sh, x_epsg31467, y_epsg31467, ts, id = id, month = month, 
                crs = 31467)

A note on pipes (|>)

amt was heavily inspired through workflows suggested by the popular packages from the tidyverse. The above steps could easily be connected using pipes. Note that result will be exactly the same.

data(sh)
tr2 <- sh |> filter(complete.cases(sh)) |> 
  mutate(
    ts = as.POSIXct(lubridate::ymd(day) + lubridate::hms(time)), 
    id = "Animal 1", 
    month = lubridate::month(ts)
  ) |> 
  filter(!duplicated(ts)) |> 
  make_track(x_epsg31467, y_epsg31467, ts, id = id, month = month, 
           crs = 31467)
tr2
## # A tibble: 1,493 × 5
##         x_      y_ t_                  id       month
##  *   <int>   <int> <dttm>              <chr>    <dbl>
##  1 3558528 5999094 2008-03-30 00:01:47 Animal 1     3
##  2 3558513 5999055 2008-03-30 06:00:54 Animal 1     3
##  3 3558564 5999146 2008-03-30 12:01:47 Animal 1     3
##  4 3558504 5999072 2008-03-30 18:01:24 Animal 1     3
##  5 3558495 5999051 2008-03-30 18:25:56 Animal 1     3
##  6 3558493 5999052 2008-03-30 18:26:05 Animal 1     3
##  7 3558489 5999051 2008-03-30 18:26:14 Animal 1     3
##  8 3558486 5999046 2008-03-30 18:26:24 Animal 1     3
##  9 3558484 5999052 2008-03-30 18:26:33 Animal 1     3
## 10 3558317 5998989 2008-03-30 18:38:01 Animal 1     3
## # … with 1,483 more rows

Working with tracks

Utility functions

Basic manipulation

Remember, that a track_xy* behaves like regular a data.frame. This means that we can use all data manipulation verbs that we are used to from base R or the tidyverse. For example, we can filter a track based on some characteristic. As an example we extract all relocations from the month May.

tr3 <- tr2 |> filter(month == 5)

# we are left with a track
class(tr3)
## [1] "track_xyt"  "track_xy"   "tbl_df"     "tbl"        "data.frame"

Transforming CRS

If we set the CRS when creating a track (we can verify this with has_crs), we can transform the CRS of the coordinates with the function transform_coords (a wrapper around sf::st_transform()). For illustration, we will transform the CRS of tr2 to geographical coordinates (EPSG:4326).

transform_coords(tr2, 4326)
## # A tibble: 1,493 × 5
##       x_    y_ t_                  id       month
##  * <dbl> <dbl> <dttm>              <chr>    <dbl>
##  1  9.89  54.1 2008-03-30 00:01:47 Animal 1     3
##  2  9.89  54.1 2008-03-30 06:00:54 Animal 1     3
##  3  9.89  54.1 2008-03-30 12:01:47 Animal 1     3
##  4  9.89  54.1 2008-03-30 18:01:24 Animal 1     3
##  5  9.89  54.1 2008-03-30 18:25:56 Animal 1     3
##  6  9.89  54.1 2008-03-30 18:26:05 Animal 1     3
##  7  9.89  54.1 2008-03-30 18:26:14 Animal 1     3
##  8  9.89  54.1 2008-03-30 18:26:24 Animal 1     3
##  9  9.89  54.1 2008-03-30 18:26:33 Animal 1     3
## 10  9.89  54.1 2008-03-30 18:38:01 Animal 1     3
## # … with 1,483 more rows

Some initial data exploration

Several functions for calculating derived quantities are available. We will start with looking at step length. The function step_lengths can be used for this.

tr2 <- tr2 |> mutate(sl_ = step_lengths(tr2))

If we look at a summary of sl_ we note two things:

summary(tr2$sl_)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    0.00   35.01  105.33  249.07  297.75 4727.86       1

Note, 1) there is a NA for the last step length, this is expected because we are still in a point representation (i.e., there is no step length for the last relocation). 2) the range is fairly large ranging from 0 to almost 5 km. Before looking at step lengths in any further detail, we will have to make sure the sampling rate is more or less regular (i.e., the same time step between any two points).

The function summarize_sampling_rate provides an easy way to look at the sampling rate.

summarize_sampling_rate(tr2)
## # A tibble: 1 × 9
##      min    q1 median  mean    q3   max    sd     n unit 
##    <dbl> <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <int> <chr>
## 1 0.0025  2.00   2.01  6.34  6.00 3924.  102.  1492 hour

This suggests that a sampling rate for 6 hours might be adequate. We can then use the function track_resample to resample the track and only keep relocations that are approximately 6 hours apart (within some tolerance, that can be specified). We will use the function lubridate::hours to specify the sampling rate and lubridate::minutes to specify the tolerance. Both arguments rate and tolerance are expected to be a Period.

tr3 <- tr2 |> track_resample(rate = hours(6), tolerance = minutes(20))
tr3
## # A tibble: 826 × 7
##         x_      y_ t_                  id       month    sl_ burst_
##  *   <int>   <int> <dttm>              <chr>    <dbl>  <dbl>  <dbl>
##  1 3558528 5999094 2008-03-30 00:01:47 Animal 1     3  41.8       1
##  2 3558513 5999055 2008-03-30 06:00:54 Animal 1     3 104.        1
##  3 3558564 5999146 2008-03-30 12:01:47 Animal 1     3  95.3       1
##  4 3558504 5999072 2008-03-30 18:01:24 Animal 1     3  22.8       1
##  5 3557474 5999130 2008-03-31 00:01:23 Animal 1     3 155.        1
##  6 3557319 5999127 2008-03-31 06:01:45 Animal 1     3   6.08      1
##  7 3557313 5999126 2008-03-31 12:01:11 Animal 1     3   4.47      1
##  8 3557317 5999128 2008-03-31 18:01:55 Animal 1     3 113.        1
##  9 3557204 5999130 2008-04-01 00:01:24 Animal 1     4 187.        1
## 10 3557108 5999291 2008-04-01 06:00:54 Animal 1     4   6.32      1
## # … with 816 more rows

tr3 still a track, but with two differences compared to tr2. 1) the number of rows is reduced from 1493 to 826, because only relocations that are 6 hours +/- the tolerance apart of each other are retained; 2) tr3 has one new column called burst_. A burst is sequence of relocations with equal sampling rates. Consider the following hypothetical example: 5 relocations are all 6 hours apart. Then there is a gap of 12 hours because one relocation failed and afterwards then there are an other 10 relocations all 6 hours apart. Then we would consider the first 5 relocations as a burst and the second 10 relocations (after the 12 hour gap) as a second burst.

From tracks to steps

In many situations we are more interested in steps (that is the animal moving from one relocation to an other, or the straight line between a start and a end point), that in the individual relocations. amt supports steps as an other way to represent movement data. The transition from a track to steps can be done via two functions.

  1. steps(): Takes as an input a track, converts the track to step and calculating some derived quantities (e.g., step lengths, turning angles). The function steps() expects a track with regular sampling rates.
  2. steps_by_burst(): Takes as an input a resampled track (i.e., a track with several bursts) and will calculate derived quantities per burst.

How to deal with several animals

Up to now we have only considered situations with one animal. However, in most telemetry studies more than one animal are tracked and we often want to calculated movement relevant characteristics for several animals individually. amt does not provide a infrastructure for dealing with several animal, however, list-columns from the tidyverse can be used to manage many animals. Because a track is just a tibble all tidyverse verbs can be used. The general strategy consists of three steps:

  1. Nest a track by one or more columns. This retains the unique values of the grouping variable(s) and creates a new list-column with tracks.
  2. Now we can perform operations on the grouped data creating a new list column. This can be done in a combination with mutate and map (instead of map also lapply could be used).
  3. Select the relevant columns and unnest. With select() we can select columns of interest and reverse the nesting with the function unnest().

As an example we will use a second data set included in amt on tracks of four fishers. We will load the data, create a track, resample the tracks individually to 30 min and create a histogram of step lengths (accounting for bursts).

We start by loading the data and creating a track of all individuals together

data("amt_fisher")
trk <- amt_fisher |> make_track(x_, y_, t_, id = id)

Next, we group the track by id and nest the track.

trk1 <- trk |> nest(data = -"id")
trk1
## # A tibble: 4 × 2
##   id    data                  
##   <chr> <list>                
## 1 M1    <trck_xyt [919 × 3]>  
## 2 M4    <trck_xyt [8,958 × 3]>
## 3 F2    <trck_xyt [3,004 × 3]>
## 4 F1    <trck_xyt [1,349 × 3]>

We now want to resample each track to 30 minutes with a tolerance of 5 minutes and create steps for each animal. For the first animal we would do as follows:

# get the data for the first animal
x <- trk1$data[[1]]

# apply the data analysis
x |> track_resample(rate = minutes(30), tolerance = minutes(5)) |>
  steps_by_burst()
## # A tibble: 412 × 11
##    burst_      x1_     x2_    y1_    y2_   sl_ direc…¹   ta_ t1_                
##  *  <dbl>    <dbl>   <dbl>  <dbl>  <dbl> <dbl>   <dbl> <dbl> <dttm>             
##  1      1 1782673.  1.78e6 2.40e6 2.40e6 10.6   -0.427 NA    2009-02-11 12:16:45
##  2      1 1782683.  1.78e6 2.40e6 2.40e6  4.96   2.08   2.51 2009-02-11 12:45:48
##  3      1 1782681.  1.78e6 2.40e6 2.40e6  2.19   0.464 -1.62 2009-02-11 13:15:19
##  4      1 1782683.  1.78e6 2.40e6 2.40e6  7.50  -1.68  -2.15 2009-02-11 13:45:37
##  5      1 1782682.  1.78e6 2.40e6 2.40e6  8.01   1.24   2.92 2009-02-11 14:15:49
##  6      1 1782684.  1.78e6 2.40e6 2.40e6 24.2   -1.54  -2.78 2009-02-11 14:45:13
##  7      1 1782685.  1.78e6 2.40e6 2.40e6 38.8    1.98  -2.76 2009-02-11 15:15:10
##  8      1 1782669.  1.78e6 2.40e6 2.40e6 13.6   -0.825 -2.80 2009-02-11 15:45:33
##  9      1 1782679.  1.78e6 2.40e6 2.40e6  5.26   1.52   2.34 2009-02-11 16:15:14
## 10      1 1782679.  1.78e6 2.40e6 2.40e6 37.2   -1.00  -2.52 2009-02-11 16:45:52
## # … with 402 more rows, 2 more variables: t2_ <dttm>, dt_ <drtn>, and
## #   abbreviated variable name ¹​direction_p

We now want to apply exactly the same logic to all animals. We can do this by using a map and save the results to a new column using mutate.

trk2 <- trk1 |> 
  mutate(steps = map(data, function(x) 
    x |> track_resample(rate = minutes(30), tolerance = minutes(5)) |> steps_by_burst()))

trk2
## # A tibble: 4 × 3
##   id    data                   steps                
##   <chr> <list>                 <list>               
## 1 M1    <trck_xyt [919 × 3]>   <brstd_s_ [412 × 11]>
## 2 M4    <trck_xyt [8,958 × 3]> <brstd_s_ [850 × 11]>
## 3 F2    <trck_xyt [3,004 × 3]> <brstd_s_ [308 × 11]>
## 4 F1    <trck_xyt [1,349 × 3]> <brstd_s_ [413 × 11]>

Finally, we can select id and steps, unnest the new data_frame and create a plot of the step-length distributions.

trk2 |> select(id, steps) |> unnest(cols = steps) |> 
  ggplot(aes(sl_, fill = factor(id))) + geom_density(alpha = 0.4)

Session

sessioninfo::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.2.2 Patched (2022-11-10 r83330)
##  os       Ubuntu 22.04.2 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  C
##  ctype    en_US.UTF-8
##  tz       Europe/Berlin
##  date     2023-03-28
##  pandoc   2.19.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  amt         * 0.2.1.0 2023-03-28 [1] local
##  backports     1.4.1   2021-12-13 [5] CRAN (R 4.2.0)
##  bslib         0.4.2   2022-12-16 [3] CRAN (R 4.2.2)
##  cachem        1.0.7   2023-02-24 [5] CRAN (R 4.2.2)
##  checkmate     2.1.0   2022-04-21 [5] CRAN (R 4.2.0)
##  class         7.3-21  2023-01-23 [6] CRAN (R 4.2.2)
##  classInt      0.4-9   2023-02-28 [3] CRAN (R 4.2.2)
##  cli           3.6.0   2023-01-09 [5] CRAN (R 4.2.2)
##  colorspace    2.1-0   2023-01-23 [5] CRAN (R 4.2.2)
##  data.table    1.14.8  2023-02-17 [3] CRAN (R 4.2.2)
##  DBI           1.1.3   2022-06-18 [5] CRAN (R 4.2.1)
##  digest        0.6.31  2022-12-11 [3] CRAN (R 4.2.2)
##  dplyr       * 1.1.0   2023-01-29 [5] CRAN (R 4.2.2)
##  e1071         1.7-13  2023-02-01 [3] CRAN (R 4.2.2)
##  evaluate      0.20    2023-01-17 [5] CRAN (R 4.2.2)
##  fansi         1.0.4   2023-01-22 [5] CRAN (R 4.2.2)
##  farver        2.1.1   2022-07-06 [5] CRAN (R 4.2.1)
##  fastmap       1.1.1   2023-02-24 [5] CRAN (R 4.2.2)
##  generics      0.1.3   2022-07-05 [5] CRAN (R 4.2.1)
##  ggplot2     * 3.4.1   2023-02-10 [3] CRAN (R 4.2.2)
##  glue          1.6.2   2022-02-24 [5] CRAN (R 4.2.0)
##  gtable        0.3.1   2022-09-01 [5] CRAN (R 4.2.1)
##  highr         0.10    2022-12-22 [3] CRAN (R 4.2.2)
##  htmltools     0.5.4   2022-12-07 [5] CRAN (R 4.2.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.2.2)
##  jsonlite      1.8.4   2022-12-06 [3] CRAN (R 4.2.2)
##  KernSmooth    2.23-20 2021-05-03 [6] CRAN (R 4.2.0)
##  knitr         1.42    2023-01-25 [5] CRAN (R 4.2.2)
##  labeling      0.4.2   2020-10-20 [5] CRAN (R 4.2.0)
##  lattice       0.20-45 2021-09-22 [6] CRAN (R 4.2.0)
##  lifecycle     1.0.3   2022-10-07 [5] CRAN (R 4.2.1)
##  lubridate     1.9.2   2023-02-10 [3] CRAN (R 4.2.2)
##  magrittr      2.0.3   2022-03-30 [5] CRAN (R 4.2.0)
##  Matrix        1.5-3   2022-11-11 [6] CRAN (R 4.2.2)
##  munsell       0.5.0   2018-06-12 [3] CRAN (R 4.2.2)
##  pillar        1.8.1   2022-08-19 [5] CRAN (R 4.2.1)
##  pkgconfig     2.0.3   2019-09-22 [3] CRAN (R 4.2.2)
##  proxy         0.4-27  2022-06-09 [3] CRAN (R 4.2.2)
##  purrr         1.0.1   2023-01-10 [5] CRAN (R 4.2.2)
##  R6            2.5.1   2021-08-19 [5] CRAN (R 4.2.0)
##  rbibutils     2.2.13  2023-01-13 [3] CRAN (R 4.2.2)
##  Rcpp          1.0.10  2023-01-22 [5] CRAN (R 4.2.2)
##  Rdpack        2.4     2022-07-20 [3] CRAN (R 4.2.2)
##  rlang         1.0.6   2022-09-24 [5] CRAN (R 4.2.1)
##  rmarkdown     2.20    2023-01-19 [5] CRAN (R 4.2.2)
##  rstudioapi    0.14    2022-08-22 [5] CRAN (R 4.2.1)
##  sass          0.4.5   2023-01-24 [5] CRAN (R 4.2.2)
##  scales        1.2.1   2022-08-20 [5] CRAN (R 4.2.1)
##  sessioninfo   1.2.2   2021-12-06 [3] CRAN (R 4.2.2)
##  sf            1.0-9   2022-11-08 [3] CRAN (R 4.2.2)
##  survival      3.5-3   2023-02-12 [3] CRAN (R 4.2.2)
##  tibble        3.1.8   2022-07-22 [5] CRAN (R 4.2.2)
##  tidyr         1.3.0   2023-01-24 [5] CRAN (R 4.2.2)
##  tidyselect    1.2.0   2022-10-10 [5] CRAN (R 4.2.1)
##  timechange    0.2.0   2023-01-11 [5] CRAN (R 4.2.2)
##  units         0.8-1   2022-12-10 [3] CRAN (R 4.2.2)
##  utf8          1.2.3   2023-01-31 [5] CRAN (R 4.2.2)
##  vctrs         0.5.2   2023-01-23 [5] CRAN (R 4.2.2)
##  withr         2.5.0   2022-03-03 [5] CRAN (R 4.2.0)
##  xfun          0.37    2023-01-31 [5] CRAN (R 4.2.2)
##  yaml          2.3.7   2023-01-23 [5] CRAN (R 4.2.2)
## 
##  [1] /tmp/RtmpEE1aXl/Rinst11aff69feb4c3
##  [2] /tmp/RtmpTZF65G/temp_libpathf4f475febbc9
##  [3] /home/jsigner/R/x86_64-pc-linux-gnu-library/4.2
##  [4] /usr/local/lib/R/site-library
##  [5] /usr/lib/R/site-library
##  [6] /usr/lib/R/library
## 
## ──────────────────────────────────────────────────────────────────────────────