Introduction to moveprofile

Overview

The moveprofile package provides tools for classifying animal movement strategies using GPS tracking data. It implements a profile-based approach using Net Squared Displacement (NSD) metrics with bootstrap confidence intervals.

Movement Strategies

The framework is flexible regarding which movement strategies to classify. Users define their own behavioural categories based on the ecology of the study species and the research question. The classification is entirely determined by the profile data provided.

Example strategies for different taxa:

The package does not impose predefined categories. Any set of movement behaviours that can be distinguished by NSD-based metrics can be classified.

Key Features

Workflow Overview

The typical workflow consists of six steps:

  1. Create mvmt object from GPS data
  2. Calculate daily centroids to reduce noise
  3. Calculate Net Squared Displacement (NSD)
  4. Detect breakpoints and segment trajectory
  5. Classify segments using reference profiles
  6. Review and assign final classification

Installation

# Install from GitHub
devtools::install_github("kadleci/moveprofile")
library(moveprofile)
library(dplyr)
library(ggplot2)

Practical Recommendations

Based on simulation experiments and empirical validation (see Kadlec et al., in prep), we provide the following guidelines:

Profile Data Requirements

Segment Duration

Classification confidence depends on the number of observations (centroid points) in each segment:

The actual duration in days depends on your centroid interval:

Centroid interval 30 observations = 60 observations =
Daily (24h) 30 days 60 days
12-hour 15 days 30 days
6-hour 7.5 days 15 days

Temporal Resolution (Centroid Interval)

The optimal centroid interval depends on species movement dynamics:

Metric Selection

Not all metrics contribute equally across species. Examine profile data distributions to identify informative metrics:

The recommended core set of four metrics (slope, rolling_var, volatility, reversal_rate) typically provides effective discrimination across most systems.

Step 1: Create mvmt Objects

The mvmt() function creates a standardized data object for the package. You need separate objects for profile and test data.

Profile Data

Profile data requires expert-annotated segments with known behavioural classifications. The number and definition of behavioural states depend entirely on your study species and research question.

A researcher should perform this classification based on:

data_profile <- mvmt(
  data = wolf_profiles,
  x = x,
  y = y,
  id = id,
  time = timestamp,
  crs = 3035,
  profile = TRUE,
  strategy = strategy
)
#> Warning: Found 1 duplicate id-timestamp combinations.

data_profile
#> 
#> -- mvmtclass trajectory -- profile 
#> Stage: raw | CRS: 3035 
#> Strategies: dispersal, floating, residential 
#> Segments: 23 | Animals: 13 
#> 
#> # A tibble: 23 × 6
#>    segment_id  id     from       to             n strategy   
#>    <chr>       <chr>  <date>     <date>     <int> <fct>      
#>  1 103215_seg1 103215 2024-10-11 2025-03-01  1169 floating   
#>  2 103217_seg1 103217 2025-03-19 2025-10-14  2865 residential
#>  3 11614_seg1  11614  2017-11-11 2018-04-21  3432 residential
#>  4 11614_seg2  11614  2018-04-22 2018-05-27   784 dispersal  
#>  5 11614_seg3  11614  2018-05-28 2018-10-01  2772 residential
#>  6 20536_seg1  20536  2017-10-31 2019-02-23 10182 residential
#>  7 20536_seg2  20536  2019-02-24 2019-03-25  1189 dispersal  
#>  8 29456_seg1  29456  2019-05-09 2019-12-22   756 residential
#>  9 29456_seg2  29456  2019-12-23 2020-01-18    62 dispersal  
#> 10 29456_seg3  29456  2020-01-19 2021-06-16  2507 residential
#> # ℹ 13 more rows

Important: The strategy column in your profile data defines which strategies the framework will learn. These can be any categories meaningful for your species (e.g., “territorial”, “nomadic”, “dispersing” for wolves; or “breeding”, “stopover”, “migrating” for birds).

Test Data

Test data contains animals with unknown movement strategies to be classified:

# Filter to 2 animals for demonstration
data_test <- mvmt(
  data = wolf_unknown |> filter(id %in% c("36930", "103216")),
  x = x,
  y = y,
  id = id,
  time = timestamp,
  crs = 3035
)

data_test
#> 
#> -- mvmtclass trajectory -- test 
#> Stage: raw | CRS: 3035 
#> Animals: 2 
#> 
#> # A tibble: 2 × 4
#>   id     from       to             n
#>   <chr>  <date>     <date>     <int>
#> 1 103216 2024-10-22 2025-10-14  3668
#> 2 36930  2020-11-30 2021-07-08  2185

Accessing Individual Animals

The mvmt object is a named list. Access individual animals using $:

# Single animal from test data
data_test[["36930"]]
#> # A tibble: 2,185 × 4
#>    timestamp                  x        y t_days
#>    <dttm>                 <dbl>    <dbl>  <dbl>
#>  1 2020-11-30 00:00:00 4453008. 2865678.  0    
#>  2 2020-11-30 03:00:00 4453003. 2865662.  0.125
#>  3 2020-11-30 06:00:00 4453656. 2865864.  0.25 
#>  4 2020-11-30 09:00:00 4453654. 2865785.  0.375
#>  5 2020-11-30 12:01:00 4453657. 2865779.  0.501
#>  6 2020-11-30 15:00:00 4453667. 2865801.  0.625
#>  7 2020-11-30 18:00:00 4453696. 2865813.  0.75 
#>  8 2020-11-30 18:30:00 4453704. 2865805.  0.771
#>  9 2020-11-30 19:00:00 4453699. 2865806.  0.792
#> 10 2020-11-30 19:30:00 4453698. 2865862.  0.812
#> # ℹ 2,175 more rows

# Single segment from profile data
data_profile[["29456_seg1"]]
#> # A tibble: 756 × 4
#>    timestamp                 x      y t_days
#>    <dttm>                <dbl>  <dbl>  <dbl>
#>  1 2019-05-09 04:32:00 617880. 17027.  0    
#>  2 2019-05-09 17:00:00 617928. 17813.  0.519
#>  3 2019-05-10 05:31:00 617924. 17813.  1.04 
#>  4 2019-05-10 18:01:00 617925. 17826.  1.56 
#>  5 2019-05-11 06:30:00 615399. 15725.  2.08 
#>  6 2019-05-11 19:00:00 613902. 15224.  2.60 
#>  7 2019-05-12 20:00:00 609245. 19909.  3.64 
#>  8 2019-05-13 08:30:00 609601. 18283.  4.17 
#>  9 2019-05-13 21:00:00 609241. 19906.  4.69 
#> 10 2019-05-14 09:31:00 608922. 19925.  5.21 
#> # ℹ 746 more rows

# List all available IDs
names(data_test)
#> [1] "103216"   "36930"    ".summary"

Plotting Raw Data

# Plot all animals (returns list, prints all)
plot(data_test)


# Plot single animal
plot(data_test, id = "103216")

For profile data, plots also show movement strategies:

plot(data_profile, id = "29456_seg1")

Step 2: Calculate Centroids

Daily centroids reduce GPS noise and standardize temporal resolution. The optimal interval depends on species movement dynamics.

# For wolves: daily centroids
centroids_profile <- centroids(
  data = data_profile,
  interval = "daily",
  method = "mean"
)

centroids_test <- centroids(
  data = data_test,
  interval = "daily",
  method = "mean"
)

centroids_test
#> 
#> -- mvmtclass trajectory -- test 
#> Stage: centroids | CRS: 3035 
#> Interval: daily 
#> Animals: 2 
#> 
#> # A tibble: 2 × 4
#>   id     from       to             n
#>   <chr>  <date>     <date>     <int>
#> 1 103216 2024-10-22 2025-10-14   358
#> 2 36930  2020-11-30 2021-07-08   221

The interval argument controls temporal aggregation:

Interval Use case
"daily" Large mammals, species with stable daily patterns
12 Migratory birds, species with rapid behavioural transitions
6 Fine-scale behavioural analysis (more noise)

Example for migratory birds:

# For lapwings: 12-hour centroids to capture faster transitions
centroids_lapwing <- centroids(data_lapwing, interval = 12)

Step 3: Calculate NSD

Net Squared Displacement (NSD) is the squared distance from the starting point:

nsd_profile <- nsd(centroids_profile, type = "2D")
nsd_test <- nsd(centroids_test, type = "2D")

nsd_test
#> 
#> -- mvmtclass trajectory -- test 
#> Stage: nsd | CRS: 3035 
#> Interval: daily 
#> Animals: 2 
#> 
#> # A tibble: 2 × 4
#>   id     from       to             n
#>   <chr>  <date>     <date>     <int>
#> 1 103216 2024-10-22 2025-10-14   358
#> 2 36930  2020-11-30 2021-07-08   221

Plotting NSD

Three plot types are available:

# NSD over time (default after nsd())
plot(nsd_test, id = "36930")


# Both side by side
plot(nsd_test, id = "103216", type = "both")

# Plot all animals
plot(nsd_test)

Interactive Plot for Breakpoint Selection

For animals with complex trajectories, use the interactive plot to identify suitable breakpoint ranges:

plot_interactive(data = nsd_test, id = "103216")

This opens a plotly widget where you can zoom, pan, and read exact time values to define bp_range for segmentation.

Step 4: Breakpoint Detection

The breakpoints() function segments the trajectory using grid search with AIC optimization:

# Animal with multiple strategies - specify bp_range
animal_bp <- nsd_test |>
  breakpoints(
    id = "103216", 
    bp_range = list(c(150, 220), c(220, 300)),
    step_size = 1,
    min_points = 10,
    gap = 1,
    plot = FALSE
  ) |> 
  # Animal with single strategy - no bp_range needed
  breakpoints(id = "36930", plot = FALSE)
#> Processing: 103216 
#> Points: 358 
#> Searching for 2 breakpoint(s)
#> Testing 5748 combinations
#> Successful fits: 5696 / 5748 
#> Best breakpoints: 183, 220 
#> Segment distribution:
#>    segment1 : 184 points
#>    segment2 : 37 points
#>    segment3 : 137 points
#> Processing: 36930 
#> No breakpoints requested - single segment

animal_bp
#> 
#> -- mvmtclass trajectory -- test 
#> Stage: segmented | CRS: 3035 
#> Interval: daily 
#> Animals: 2 
#> 
#> # A tibble: 2 × 4
#>   id     from       to             n
#>   <chr>  <date>     <date>     <int>
#> 1 103216 2024-10-22 2025-10-14   358
#> 2 36930  2020-11-30 2021-07-08   221

Breakpoint Arguments

Argument Description
bp_range List of search ranges for each breakpoint, e.g., list(c(50, 150), c(200, 300))
step_size Grid search step in days (default: 2)
gap Minimum distance between breakpoints (default: 10)
min_points Minimum points per segment (default: 3)
plot Show result plot (default: TRUE)

Step 5: Classification

Reference Profiles

View the metric profiles for each movement strategy:

selected_metrics <- c("slope", "r_squared", "rolling_var",
                      "volatility", "reversal_rate", "autocorrelation")

profiles(nsd_profile, metrics = selected_metrics)
#> # A tibble: 18 × 5
#>    strategy    n_segments metric              mean      sd
#>    <chr>            <int> <chr>              <dbl>   <dbl>
#>  1 floating             5 slope            1.50e+0 2.51e+0
#>  2 floating             5 r_squared        4.86e-2 4.43e-2
#>  3 floating             5 rolling_var      1.68e+6 2.87e+6
#>  4 floating             5 volatility       6.03e+2 8.34e+2
#>  5 floating             5 reversal_rate    4.95e-1 6.99e-2
#>  6 floating             5 autocorrelation  6.55e-1 1.53e-1
#>  7 residential         13 slope           -1.89e-1 7.38e-1
#>  8 residential         13 r_squared        3.74e-2 4.08e-2
#>  9 residential         13 rolling_var      4.35e+4 9.72e+4
#> 10 residential         13 volatility       8.88e+1 1.32e+2
#> 11 residential         13 reversal_rate    5.91e-1 5.38e-2
#> 12 residential         13 autocorrelation  3.59e-1 2.16e-1
#> 13 dispersal            5 slope            2.96e+3 2.96e+3
#> 14 dispersal            5 r_squared        8.59e-1 6.29e-2
#> 15 dispersal            5 rolling_var      2.11e+8 3.37e+8
#> 16 dispersal            5 volatility       3.77e+3 4.10e+3
#> 17 dispersal            5 reversal_rate    3.18e-1 1.09e-1
#> 18 dispersal            5 autocorrelation  8.85e-1 3.24e-2

Available Metrics

The framework extracts NSD-based metrics that capture different aspects of movement behaviour:

Metric Description Ecological interpretation
slope Linear trend of NSD over time (signed) Positive = directional movement away; Near zero = residency
slope_abs Absolute value of linear NSD trend Magnitude of directional movement, regardless of direction
r_squared R² of linear model High = consistent directional trend; Low = variable movement
rolling_var Mean variance over sliding window High = unstable/exploratory; Low = consistent behaviour
volatility Mean absolute change between consecutive NSD High = erratic movement; Low = smooth trajectories
reversal_rate Proportion of directional changes High = oscillatory (residency); Low = unidirectional
autocorrelation Lag-1 autocorrelation High = persistent movement; Low = random changes

Recommended core metrics (effective across most systems):

selected_metrics <- c("slope", "rolling_var", "volatility", "reversal_rate")

Extended set (if needed for finer discrimination):

selected_metrics <- c("slope", "r_squared", "rolling_var",
                      "volatility", "reversal_rate", "autocorrelation")

Tip: Examine metric distributions in your profile data using profiles() to identify which metrics provide the best separation between your behavioural categories.

Classify Segments

# Without bootstrap (fastest)
classify(
  profiles = nsd_profile, 
  unknown_data = animal_bp, 
  id = "36930",
  metrics = selected_metrics, 
  bootstrap = NULL
)
#> Processing 1 animal(s)
#> Strategies: floating, residential, dispersal 
#> Metrics: slope, r_squared, rolling_var, volatility, reversal_rate, autocorrelation 
#> Bootstrap: none
#> ------------------------------------------------
#> 
#> Classifying: 36930 (Segments: 1 )
#>   Processing: segment1 
#>   -> residential 
#> 
#> Done!
#> # A tibble: 1 × 9
#>   id    segment  t_start    t_end      class    credibility floating residential
#>   <chr> <chr>    <date>     <date>     <chr>    <chr>       <chr>    <chr>      
#> 1 36930 segment1 2020-11-30 2021-07-08 residen… <NA>        42.6%    56.6%      
#> # ℹ 1 more variable: dispersal <chr>
#> # A tibble: 4 × 9
#>   id     segment  t_start    t_end      class   credibility floating residential
#>   <chr>  <chr>    <date>     <date>     <chr>   <chr>       <chr>    <chr>      
#> 1 103216 segment1 2024-10-22 2025-04-23 reside… High        30.2% [… 69.6% [51.…
#> 2 103216 segment2 2025-04-24 2025-05-30 disper… High        0.0% [0… 0.0% [0.0%…
#> 3 103216 segment3 2025-05-31 2025-10-14 floati… High        89.6% [… 6.9% [0.0%…
#> 4 36930  segment1 2020-11-30 2021-07-08 reside… Medium      35.2% [… 64.3% [44.…
#> # ℹ 1 more variable: dispersal <chr>
# Double bootstrap - resamples both reference and unknown (slower)
results <- classify(
  profiles = nsd_profile, 
  unknown_data = animal_bp,
  metrics = selected_metrics, 
  bootstrap = "double", 
  n_boot = 100
)

Bootstrap Options

Option Description Output Format Use case
NULL No bootstrap "73.3%" Quick exploration
"single" Resample reference only "81.1% [63.1%, 99.4%]" Standard analysis
"double" Resample both "82.4% [70.1%, 90.3%]" Full uncertainty quantification

Interpreting confidence intervals:

Wide confidence intervals are not methodological failures — they honestly acknowledge insufficient information or genuine ecological ambiguity (e.g., transitional periods, mixed behaviours).

Recommendation: For datasets with small profile samples (<15–20 segments total), use single bootstrap to avoid instability from resampling already-limited data.

Step 6: Review and Assign

Automatic Assignment

By default, classify() performs automatic assignment (automatic = TRUE). This assigns the best matching strategy to a class column and evaluates the reliability of this assignment in a credibility column based on the overlap of confidence intervals:

If you prefer to assign all segments manually, you can set automatic = FALSE in the classify() function, which leaves the class and credibility columns as NA.

Interactive Review

For more control or to override automatic assignments, use review() for interactive assignment:

final <- review(results)

The console displays each segment for review:

═══════════════════════════════════════════════════════════════
  INTERACTIVE CLASSIFICATION REVIEW
═══════════════════════════════════════════════════════════════

Available strategies:
  1 = residential
  2 = dispersal
  3 = floating
  0 = unassigned (skip)
  q = quit review

Starting review of 4 segment(s)...
Press Enter to accept suggested classification.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Animal: 103216 | Segment: segment1
Period: 2024-10-22 to 2025-04-23
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PROBABILITIES:
  1. residential   77.2% [69.0%, 87.6%]
  2. dispersal     0.4% [0.1%, 1.1%]
  3. floating      22.4% [12.2%, 30.7%]

Suggested: residential
Your choice [Enter=1, 0-3, q]: 

For each segment, you see:

Enter a number to assign a strategy, or press Enter to accept the suggestion.

Add Classification to GPS Data

After classification, add the assigned strategies to GPS data:

# Using results directly (without review) - uses highest probability
gps_classified <- add_class(animal_bp, results = results)

# When using bootstrapping, we recommend using review to maximize the assignment of appropriate strategies
# gps_classified <- add_class(animal_bp, results = final)


gps_classified
#> 
#> -- mvmtclass trajectory -- test 
#> Stage: classified | CRS: 3035 
#> Interval: daily 
#> Animals: 2 
#> 
#> # A tibble: 2 × 4
#>   id     from       to             n
#>   <chr>  <date>     <date>     <int>
#> 1 103216 2024-10-22 2025-10-14   358
#> 2 36930  2020-11-30 2021-07-08   221

This adds a classification column to each GPS point based on segment assignments.

Visualize Classified Trajectory

# Plot classified trajectory with movement strategies
plot(gps_classified, id = "103216", type = "both")

Integration with amt

Convert to amt format for home range analysis, step selection functions, etc.:

library(amt)

track <- mvmt_to_amt(gps_classified, id = "36930")

# Home range estimation
track |> hr_kde()
track |> hr_mcp()

Complete Workflow Example

library(moveprofile)
library(dplyr)

# 1. Create mvmt objects
prof <- mvmt(wolf_profiles, id = id, time = timestamp, x = x, y = y,
             crs = 3035, profile = TRUE, strategy = strategy)

test <- mvmt(wolf_unknown, id = id, time = timestamp, x = x, y = y,
             crs = 3035)

# 2. Calculate centroids
prof_c <- centroids(prof, interval = "daily")
test_c <- centroids(test, interval = "daily")

# 3. Calculate NSD
nsd_prof <- nsd(prof_c)
nsd_test <- nsd(test_c)

# 4. Segment trajectory
plot_interactive(nsd_test, id = "103216")  # Find bp_range

segmented <- nsd_test |>
  breakpoints(id = "103216", plot = TRUE,
              bp_range = list(c(150, 220), c(220, 300)),
              step_size = 1,
              min_points = 10,
              gap = 1
  ) |>
  breakpoints(id = "36930")

# 5. Classify
metrics <- c("slope", "r_squared", "rolling_var",
             "volatility", "reversal_rate", "autocorrelation")

results <- classify(unknown_data = segmented, profiles = nsd_prof,
                    metrics = metrics,
                    bootstrap = "double", n_boot = 1000)

# 6. Review and finalize
final <- review(results)
classified <- add_class(segmented, final)

# Export or further analysis
library(amt)
track <- mvmt_to_amt(classified, id = "36930")
track |> hr_kde()

For methodology details, see the accompanying publication.