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.
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.
The typical workflow consists of six steps:
Based on simulation experiments and empirical validation (see Kadlec et al., in prep), we provide the following guidelines:
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 |
The optimal centroid interval depends on species movement dynamics:
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.
The mvmt() function creates a standardized data object
for the package. You need separate objects for profile and test
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 rowsImportant: 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 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 2185The 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"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 221The 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:
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 221Three plot types are available:
For animals with complex trajectories, use the interactive plot to identify suitable breakpoint ranges:
This opens a plotly widget where you can zoom, pan, and read exact
time values to define bp_range for segmentation.
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| 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) |
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-2The 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):
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.
# 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>
| 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.
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.
For more control or to override automatic assignments, use
review() for interactive assignment:
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.
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 221This adds a classification column to each GPS point
based on segment assignments.
Convert to amt format for home range analysis, step selection functions, etc.:
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.