| Title: | An R package for calculating movement-based metrics |
|---|---|
| Description: | An R package for calculating movement-based metrics. |
| Authors: | Mikkel Roald-Arbøl [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-9998-0058>) |
| Maintainer: | Mikkel Roald-Arbøl <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.5.0.9006 |
| Built: | 2026-10-14 14:25:36 UTC |
| Source: | https://github.com/animovement/animetric |
Computes translational and rotational kinematic measures from movement data, and returns the frame with a column for each. Handles data in any coordinate system by automatically converting to Cartesian for the calculations, then converting back to the original system.
add_kinematics(data, vertical = NULL, min_step = "auto")add_kinematics(data, vertical = NULL, min_step = "auto")
data |
An anipoint with position coordinates (x, x/y or x/y/z for Cartesian; rho/phi for polar; rho/phi/z for cylindrical; rho/phi/theta for spherical) and an index. The axes and the index are read from the frame's declared variables, so the columns can have any name. |
vertical |
|
min_step |
It affects only the direction measures ( |
The function preserves the original coordinate system by:
Detecting the input coordinate system from metadata
Converting to Cartesian if necessary
Computing kinematics in Cartesian space
Converting back to the original coordinate system
All kinematic calculations are performed using numerical differentiation
via the differentiate function. Course is unwrapped before it is
differentiated, so the turning rate has no jump at +/-pi. The turning
speed in 3D needs no angle to unwrap, and has no singularity when travel
is vertical.
When a point barely moves, the direction between successive positions is
set by sub-pixel tracking noise, and swings at random from frame to
frame. Every swing is turning to the measures above, so a resting animal
accumulates turning as fast as a running one. min_step sets the step
below which the direction is not trusted.
The step of a row is the distance its velocity implies moving per sample:
speed times the sampling interval, which for the central differences
used here is half the distance between the positions either side. Course
is measured against the horizontal part of the step alone.
With min_step = "auto", the threshold is three times the positional
noise, , estimated for each trajectory from its second
differences . For white noise of standard
deviation on each axis these have standard deviation
. Only their component along the direction of travel
is used, which a path turning at constant speed leaves at zero, and their
spread is measured with stats::mad(), which a minority of large values
barely moves. A point standing still under white noise makes steps longer
than with a probability of about 0.01% (0.04% in 3D), so
the threshold removes the directions of a stationary point while keeping
those of real movement.
The threshold is also at most half the trajectory's median step (among steps that have a direction), so most steps always keep theirs. This matters when the sampling is coarse for the movement: a random walk recorded step by step has second differences as large as its steps, and the estimate would take the movement itself for noise.
The estimate assumes the sampling is fast compared with changes in speed,
as it is for video tracking. Two cases call for setting min_step
yourself:
Smoothed positions. Smoothing turns noise into slow wander, which second differences see less of, so the estimate is smaller than the jitter left in the data, and the threshold removes only part of it.
Coarse sampling, such as GPS fixes minutes apart, where speed
changes a lot between samples. The estimate then includes real
movement, so the threshold may be too large; use the positional
precision instead, or 0.
The threshold is not stored in the result. compute_min_step() returns
the one "auto" chooses for each trajectory, with the noise estimate,
and a number given as min_step is used as it is.
An anipoint in the same coordinate system as the input, with added kinematic measures. Translational measures are computed for 1D, 2D and 3D data:
v_x, v_y, v_z
Velocity components, named by axis role whatever the input columns are called.
a_x, a_y, a_z
Acceleration components, the second derivatives of the positions.
speedThe size of the velocity.
accelerationThe signed rate of change of speed: positive
when speeding up, negative when slowing down. This is the tangential
acceleration, along the path. It is not the size of the acceleration
vector (a_x, a_y), which also includes the part that turns the
path: an animal rounding a corner at constant speed has an
acceleration of 0 but non-zero a_x and a_y.
cumulative_distanceDistance travelled since the first row: the straight-line steps between successive positions, summed. A step to or from a missing position adds nothing.
For 2D and 3D data, measures of how the direction of travel changes are added too:
turning_speedHow fast the direction of travel turns, in any direction: curvature times speed. In 2D it is the size of the turning rate. In 3D it also counts climbing and diving, and is the angle between the velocities either side of each row, over the time between them.
cumulative_turningTurning accumulated since the first row:
the angle between successive velocities, summed. A turn made while
stationary, or while moving less than min_step, is counted when
the animal moves off, as the angle between the directions before
and after.
In 2D, and in 3D when vertical is given, the measures that need a
direction to count from:
courseDirection of travel in the horizontal plane (in 2D, the
plane of the data): atan2(v_y, v_x) in 2D. It is NA where there is
no horizontal movement, or less than min_step per sample, since
the direction is then undefined.
course_unwrapped is the same, without jumps at +/-pi.
course_elevation3D only: the angle of travel above the horizontal plane, from -pi/2 (straight down) to pi/2 (straight up).
turning_rateSigned rate of change of the course, the
derivative of course_unwrapped. In 3D it counts horizontal turning
only.
turning_accelerationRate of change of the turning rate.
These describe the path, not the body: course is where the animal is going, not where it is facing, and the two differ for any animal that does not move nose-first. The names heading and angular velocity are kept for body orientation. 1D data gets translational measures only.
Angular measures are in the frame's declared unit_angle, radians or
degrees; turning rate and acceleration are per unit of the index.
Signed angles (course, turning rate) follow the frame's own
convention: course counts from the x axis toward the y axis, which
is counter-clockwise when anicore::get_angle_direction() says so (y
pointing up, as aniread leaves image data) and clockwise in a frame whose
y points down. To change convention, change the coordinates, for
example with anicore::reflect_axis(), and the angles follow. In 3D,
course counts about vertical by the right-hand rule, from the next
axis in the cycle x, y, z: from x toward y when z is vertical, from
z toward x when y is, from y toward z when x is.
add_tortuosity() for windowed measures of how winding the path
is, and summarise_path() for measures of each whole trajectory.
# 2D Cartesian data traj_2d <- data.frame(time = 0:10, x = rnorm(11), y = rnorm(11)) |> anicore::as_anipoint() kinematics_2d <- add_kinematics(traj_2d) # Polar data (automatically converted and converted back) traj_polar <- anispace::map_to_polar(traj_2d) kinematics_polar <- add_kinematics(traj_polar) # 3D data with z pointing up: course and elevation of travel traj_3d <- data.frame( time = 0:10, x = cos(0:10 / 2), y = sin(0:10 / 2), z = 0:10 / 10 ) |> anicore::as_anipoint() kinematics_3d <- add_kinematics(traj_3d, vertical = "z") # A threshold of your own, in the frame's spatial unit, or 0 to count # every direction however short the step kinematics_2d <- add_kinematics(traj_2d, min_step = 0.1)# 2D Cartesian data traj_2d <- data.frame(time = 0:10, x = rnorm(11), y = rnorm(11)) |> anicore::as_anipoint() kinematics_2d <- add_kinematics(traj_2d) # Polar data (automatically converted and converted back) traj_polar <- anispace::map_to_polar(traj_2d) kinematics_polar <- add_kinematics(traj_polar) # 3D data with z pointing up: course and elevation of travel traj_3d <- data.frame( time = 0:10, x = cos(0:10 / 2), y = sin(0:10 / 2), z = 0:10 / 10 ) |> anicore::as_anipoint() kinematics_3d <- add_kinematics(traj_3d, vertical = "z") # A threshold of your own, in the frame's spatial unit, or 0 to count # every direction however short the step kinematics_2d <- add_kinematics(traj_2d, min_step = 0.1)
Computes, for each point, the distance to the nearest point belonging to a different entity — typically a different individual at the same moment.
Which columns carry time and position is read from the anipoint's
declared variables (see anicore::get_variables()). The identity columns
are assigned roles by you, explicitly, because "another animal" and
"another point on this animal" are different questions and the data
cannot tell which one you mean.
add_nnd(data, across, n = 1L, within = NULL, focal = NULL, neighbour = NULL)add_nnd(data, across, n = 1L, within = NULL, focal = NULL, neighbour = NULL)
data |
An anipoint. |
across |
Column whose value must differ between a point and its neighbour. |
n |
Which neighbour to return (1 = nearest, 2 = second nearest).
Ranked by entity, not by point: with |
within |
Identity columns that must match, added to the temporal context. |
focal |
Named list restricting which points are measured from, e.g.
|
neighbour |
Named list restricting which points may be returned as
a neighbour, e.g. |
Every identity variable has one of three roles:
across — its value must differ between a point and its
neighbour. This is what "another" means: "individual" for the
nearest other animal, "keypoint" for the nearest other point on the
same animal.
within — its value must match. Added to the temporal context,
which always applies: points are never compared across timepoints,
observations, sessions or trials.
unnamed — unconstrained. Any value may match any other, which is what makes the default any-keypoint-to-any-keypoint.
focal and neighbour then restrict which points are measured from
and which are eligible to be measured to. Both are named lists of
column to permitted values, and they are independent, so asymmetric
questions like nose-to-tail are expressible.
The input anipoint with added columns, each named nnd_, then
the neighbour rank n, then what the column holds:
nnd_<n>_<across> — which entity the n-th nearest neighbour belongs
to (e.g. nnd_1_individual)
nnd_<n>_<variable> — the neighbour's value for each unconstrained
identity variable (e.g. nnd_1_keypoint)
nnd_<n>_distance — the distance to it
The rank is always there, 1 included, so the nearest and
second-nearest neighbours can sit side by side: call add_nnd() once
with n = 1 and again with n = 2. Because the rank sits between two
underscores and is only digits, the names split unambiguously with
"^nnd_(\\d+)_(.+)$", even when the part after it contains underscores,
as an across column named track_id gives nnd_1_track_id.
compute_nnd() for the vector-level function.
data <- anicore::example_anipoint( n_obs = 5, n_individuals = 3, n_keypoints = 3 ) # Nearest other individual, any keypoint to any keypoint data |> add_nnd(across = "individual") # Whose neck is my head nearest to? data |> add_nnd( across = "individual", focal = list(keypoint = "head"), neighbour = list(keypoint = "neck") ) # Nearest keypoint within each individual data |> add_nnd(across = "keypoint", within = "individual") # Each keypoint to the same keypoint on the nearest other individual data |> add_nnd(across = "individual", within = "keypoint") # Nearest and second-nearest other individual, side by side data |> add_nnd(across = "individual", n = 1) |> add_nnd(across = "individual", n = 2)data <- anicore::example_anipoint( n_obs = 5, n_individuals = 3, n_keypoints = 3 ) # Nearest other individual, any keypoint to any keypoint data |> add_nnd(across = "individual") # Whose neck is my head nearest to? data |> add_nnd( across = "individual", focal = list(keypoint = "head"), neighbour = list(keypoint = "neck") ) # Nearest keypoint within each individual data |> add_nnd(across = "keypoint", within = "individual") # Each keypoint to the same keypoint on the nearest other individual data |> add_nnd(across = "individual", within = "keypoint") # Nearest and second-nearest other individual, side by side data |> add_nnd(across = "individual", n = 1) |> add_nnd(across = "individual", n = 2)
Works out which way a body faces from where its points are, and declares
it as the frame's orientation: heading in 2D, a unit quaternion
(qw, qx, qy, qz) in 3D. Once declared, it is a proper where
variable that later steps can use, such as egocentric alignment by
orientation in anispace.
The body frame follows anicore's: an orientation of zero faces +x.
from -> to is the forward axis, e.g.
tail to head.
perpendicular = TRUE)from -> to runs from
the body's right to its left, e.g. right eye to left eye, and forward
is perpendicular to it.
In 3D, two points give a direction but not the roll about it, so a third,
plane, is needed. Any point off the line through from and to will
do: it fixes which way the body's y axis points (along the body), or its
forward axis (across the body). A point on the animal's left makes y its
left; a point on its back makes y point dorsally instead. Either way the
orientation is fully defined; only what its roll is called depends on the
choice.
add_orientation( data, from, to, plane = NULL, perpendicular = FALSE, attach_to = NULL, level = NULL, name = NULL, overwrite = FALSE )add_orientation( data, from, to, plane = NULL, perpendicular = FALSE, attach_to = NULL, level = NULL, name = NULL, overwrite = FALSE )
data |
A 2D or 3D anipoint with Cartesian coordinates. |
from, to
|
Members of |
plane |
In 3D, a member of |
perpendicular |
Whether |
attach_to |
Members of |
level |
The identity variable |
name |
The orientation column: one name in 2D, four in 3D ( |
overwrite |
Whether to replace orientation values already present in the rows being written, or an orientation declared in other columns. |
The orientation is computed for each subject at each moment: each
combination of the frame's other identity variables, its temporal keys and
its index. Where a defining point is missing, or the points coincide, it is
NA. Rows not named by attach_to keep what they had, so successive calls
can build up orientations for different parts in the one declared column.
In 2D, heading is in the frame's unit_angle, counting from the x
axis toward the y axis, in (-pi, pi]. Across the body, forward is the
from -> to axis turned a quarter turn, so that to is on the body's
left. In 3D the quaternion maps the body's axes into the frame's, and is
built with anispace::quat_from_vectors().
The anipoint, with the orientation columns written and declared.
af <- anicore::example_anipoint(n_obs = 5, n_individuals = 2, n_keypoints = 3) # The body faces from the shoulder to the head add_orientation(af, from = "shoulder_right", to = "head", level = "keypoint") # Attached to the head only add_orientation( af, from = "shoulder_right", to = "head", attach_to = "head", level = "keypoint" )af <- anicore::example_anipoint(n_obs = 5, n_individuals = 2, n_keypoints = 3) # The body faces from the shoulder to the head add_orientation(af, from = "shoulder_right", to = "head", level = "keypoint") # Attached to the head only add_orientation( af, from = "shoulder_right", to = "head", attach_to = "head", level = "keypoint" )
Derives a new member of an identity level at each moment, from the members
it collapses, and appends it to the frame. The rest of the data is
returned untouched. The centroid of an animal's keypoints is the usual
case; method chooses how the point is derived.
Which levels are collapsed is the caller's choice. On pose data for a
team, collapsing "keypoint" gives each player a point of their own;
across = "individual" gives one point per keypoint across the players;
and collapsing both gives the single point the whole team occupies.
A level that did not actually vary keeps its value rather than taking the new member's name — an individual's strain is still its strain, since nothing was derived over it.
The new member is an ordinary member of its level: add_kinematics()
gives it kinematics, and summarise_aniframe() summarises it alongside the
tracked points.
add_point( data, across = NULL, method = "centroid", include = NULL, exclude = NULL, name = NULL )add_point( data, across = NULL, method = "centroid", include = NULL, exclude = NULL, name = NULL )
data |
An anipoint with Cartesian coordinates. |
across |
Identity variables to collapse — the dimensions the new point is derived over. Required when the frame declares more than one identity variable, since their order is not a hierarchy and there is no finest one to assume; with a single identity variable, that one is the default. Collapsing every level gives a single point per position. |
method |
How each coordinate is derived from the members' values:
Missing values are ignored; a moment where every member is missing gives
|
include, exclude
|
Values of the collapsed level to keep or leave out.
Only meaningful when one level is collapsed. A midpoint is the centroid
of two members: |
name |
Name for the new member. Defaults to |
A declared orientation is derived too: the circular mean of the members'
yaw, in the frame's unit_angle, or in 3D the mean of their quaternions
(anispace::quat_mean()). It is weighted by confidence when method is
"weighted", and unweighted otherwise. The new member has no confidence of
its own, so its confidence is NA.
The anipoint, with the new member appended as extra rows. The collapsed identity column comes back as a factor, since it now holds a named member that an integer column could not.
compute_point() for the new member's rows alone.
af <- anicore::example_anipoint(n_obs = 20, n_individuals = 2, n_keypoints = 3) # Each animal gains a centroid add_point(af, across = "keypoint") # A median instead, robust to a stray keypoint add_point(af, across = "keypoint", method = "median") # The midpoint of two keypoints add_point(af, across = "keypoint", include = c("head", "neck"), name = "neck_head") # A custom rule add_point(af, across = "keypoint", method = \(x) mean(x, trim = 0.1), name = "trimmed") # One point per keypoint, across the animals add_point(af, across = "individual")af <- anicore::example_anipoint(n_obs = 20, n_individuals = 2, n_keypoints = 3) # Each animal gains a centroid add_point(af, across = "keypoint") # A median instead, robust to a stray keypoint add_point(af, across = "keypoint", method = "median") # The midpoint of two keypoints add_point(af, across = "keypoint", include = c("head", "neck"), name = "neck_head") # A custom rule add_point(af, across = "keypoint", method = \(x) mean(x, trim = 0.1), name = "trimmed") # One point per keypoint, across the animals add_point(af, across = "individual")
Computes how winding the path is (straightness, sinuosity and E_max) over a window centred on each row, and returns the frame with a column for each. Everything they need is computed internally and not added, so only the three measures appear.
add_tortuosity(data, window_width = 11L, step_length = "auto")add_tortuosity(data, window_width = 11L, step_length = "auto")
Straightness is appropriate for directed/goal-oriented movement, while sinuosity and E_max are appropriate for random search paths.
Works on 1D, 2D and 3D Cartesian data, reading the axes from the frame's declared variables.
The window is centred on each timepoint. Near the ends of a trajectory,
where the window would run past the first or last observation, the metrics
are NA.
Straightness is the distance between the positions at the ends of the
window over the distance travelled between them. It is NA for a window
that holds a missing position: how far the animal moved across the gap is
not known, so neither is the distance travelled.
Sinuosity and E_max come from the turning angles of the path
rediscretised to a constant step length, as Benhamou (2004) defines
sinuosity: walking along the path, a new point is placed wherever it
first leaves a circle of that radius around the last one (Bovet &
Benhamou 1988). By default (step_length = "auto") the step length is
the trajectory's mean step between rows, weighted by step length: the
average step over the distance travelled, which time spent still does not
shorten. Sinuosity and E_max describe the path at the scale of that step,
and the automatic step differs between trajectories, so to compare
trajectories, give them all the same step_length. Tracking
jitter that stays within the circle while an animal is still gives no
steps and no turning, where turning angles between successive frames
would be dominated by it. Each window takes the turning at the
rediscretised points the path reaches within it, and is NA when there
are none: where the animal moved less than a step, its path has no
sinuosity to measure. The path is rediscretised once per trajectory, and
missing positions break it into stretches that are rediscretised
separately.
The input anipoint with three columns added, named with the
window width, so that window_width = 11 gives:
straightness_11Straightness index (D/L), from 0 to 1. NA
where a position in the window is missing.
sinuosity_11Corrected sinuosity index (Benhamou 2004).
e_max_11Maximum expected displacement (dimensionless).
The width in the name keeps these windowed measures apart from the
whole-path straightness, sinuosity and e_max of summarise_path(),
and lets several window widths sit side by side.
Batschelet, E. (1981). Circular statistics in biology. Academic Press.
Bovet, P., & Benhamou, S. (1988). Spatial analysis of animals' movements using a correlated random walk model. Journal of Theoretical Biology, 131(4), 419-433. doi:10.1016/S0022-5193(88)80038-9.
Benhamou, S. (2004). How to reliably estimate the tortuosity of an animal’s path: straightness, sinuosity, or fractal dimension?. Journal of Theoretical Biology, 229(2), 209-220. doi:10.1016/j.jtbi.2004.03.016.
Cheung, A., Zhang, S., Stricker, C., & Srinivasan, M. V. (2007). Animal navigation: the difficulty of moving in a straight line. Biological Cybernetics, 97(1), 47-61. doi:10.1007/s00422-007-0158-0.
add_kinematics() for speed, course and the turning measures.
summarise_path() for the same measures over each whole trajectory.
data <- anicore::example_anipoint(n_obs = 30, n_individuals = 1, n_keypoints = 1) data |> add_tortuosity(window_width = 11) # Several window widths side by side data |> add_tortuosity(window_width = 5) |> add_tortuosity(window_width = 11) # Sinuosity of every trajectory at one scale, in the frame's spatial unit data |> add_tortuosity(window_width = 11, step_length = 0.5)data <- anicore::example_anipoint(n_obs = 30, n_individuals = 1, n_keypoints = 1) data |> add_tortuosity(window_width = 11) # Several window widths side by side data |> add_tortuosity(window_width = 5) |> add_tortuosity(window_width = 11) # Sinuosity of every trajectory at one scale, in the frame's spatial unit data |> add_tortuosity(window_width = 11, step_length = 0.5)
Compute E_max (maximum expected displacement) from pre‑computed vectors
compute_emax(mean_cos_turning, mean_step_length = NULL, dimensional = FALSE)compute_emax(mean_cos_turning, mean_step_length = NULL, dimensional = FALSE)
mean_cos_turning |
Numeric vector of mean cosine of turning angles. |
mean_step_length |
Numeric vector of mean step lengths (required if |
dimensional |
Logical. If |
Numeric vector of E_max values (same length as mean_cos_turning),
with NA for invalid inputs and Inf for perfectly straight paths.
compute_emax(mean_cos_turning = 0.8) # The dimensional form also accounts for step length compute_emax(0.8, mean_step_length = 1.2, dimensional = TRUE)compute_emax(mean_cos_turning = 0.8) # The dimensional form also accounts for step length compute_emax(0.8, mean_step_length = 1.2, dimensional = TRUE)
This helper implements a finite-difference scheme based on the Fornberg formula for interior points when the spacing between coordinates is non-uniform. End points use first-order forward/backward differences.
compute_gradient(values, coords = NULL)compute_gradient(values, coords = NULL)
values |
Numeric vector of function values. |
coords |
Numeric vector of the corresponding coordinate values.
Must be the same length as |
A numeric vector of the same length as values containing the
estimated first derivative at each point.
Fornberg, B. (1988). Generation of finite difference formulas on arbitrarily spaced grids. Mathematics of Computation, 51(184), 699-706. doi:10.1090/S0025-5718-1988-0935077-0
# The derivative of x^2 is 2x, which the interior points recover exactly compute_gradient(c(0, 1, 4, 9, 16)) # The spacing between coordinates need not be even compute_gradient(c(0, 1, 4, 9), coords = c(0, 1, 3, 6))# The derivative of x^2 is 2x, which the interior points recover exactly compute_gradient(c(0, 1, 4, 9, 16)) # The spacing between coordinates need not be even compute_gradient(c(0, 1, 4, 9), coords = c(0, 1, 3, 6))
"auto" chooses for each trajectoryReturns the threshold that min_step = "auto" uses in add_kinematics()
and summarise_path(), one per trajectory, with the positional noise it
is estimated from. Use it to see whether the automatic threshold suits
your data, to report it, or to choose a threshold of your own relative to
it.
The threshold is computed by the same code that add_kinematics() uses,
so the two always agree. See the section "The minimum step" in
add_kinematics() for how it is estimated and when to set it yourself.
compute_min_step(data)compute_min_step(data)
data |
An anipoint with two or three spatial axes, grouped one trajectory per group, as it is by its declared keys. Any coordinate system works; non-Cartesian frames are converted to Cartesian for the computation. |
A data frame with one row per trajectory: the key columns, then
positional_noise: the estimated standard deviation of the tracking
noise on each axis, in the frame's spatial unit. 0 when the
trajectory is too short, or moves too little, to estimate it.
min_step: the threshold "auto" uses, in the frame's spatial unit:
three times positional_noise, or half the trajectory's median step
if that is smaller. Where it is less than three times
positional_noise, the cap on the median step set it.
add_kinematics(), whose min_step takes the threshold.
af <- anicore::example_anipoint(n_obs = 50, n_individuals = 2, n_keypoints = 1) compute_min_step(af) # One threshold for every trajectory, so their turning is filtered alike thresholds <- compute_min_step(af) add_kinematics(af, min_step = max(thresholds$min_step))af <- anicore::example_anipoint(n_obs = 50, n_individuals = 2, n_keypoints = 1) compute_min_step(af) # One threshold for every trajectory, so their turning is filtered alike thresholds <- compute_min_step(af) add_kinematics(af, min_step = max(thresholds$min_step))
Low-level function behind add_nnd(), operating on plain vectors
for one group of comparable points (typically one timepoint).
compute_nnd( x, y, z = NULL, across, n = 1L, is_focal = NULL, is_candidate = NULL, labels = NULL )compute_nnd( x, y, z = NULL, across, n = 1L, is_focal = NULL, is_candidate = NULL, labels = NULL )
x, y
|
Numeric vectors of coordinates. |
z |
Numeric vector of coordinates, or |
across |
Vector whose value must differ between a focal point and its neighbour — the thing being ranked, e.g. individual identity. |
n |
Which neighbour to return (1 = nearest, 2 = second nearest). |
is_focal |
Logical vector marking which points to measure from, or
|
is_candidate |
Logical vector marking which points may be returned
as a neighbour, or |
labels |
Named list of vectors describing each point. The matched
neighbour's value is reported for each, as |
For each focal point, candidates are ranked by the value of across:
the closest candidate point is found for each distinct value, those
values are ranked by that distance, and the n-th is returned. So
"second nearest" means the second nearest entity, not the second
nearest point.
A tibble with nnd_across (the value of across for the
matched neighbour), one nnd_<name> column per entry of labels,
and nnd_distance.
add_nnd() for the anipoint-level function.
# Nearest point belonging to a different individual compute_nnd( x = c(0, 1, 2), y = c(0, 1, 0), across = c("a", "b", "c") ) # The second nearest, with the neighbour's label reported back compute_nnd( x = c(0, 1, 2), y = c(0, 1, 0), across = c("a", "b", "c"), n = 2L, labels = list(individual = c("a", "b", "c")) )# Nearest point belonging to a different individual compute_nnd( x = c(0, 1, 2), y = c(0, 1, 0), across = c("a", "b", "c") ) # The second nearest, with the neighbour's label reported back compute_nnd( x = c(0, 1, 2), y = c(0, 1, 0), across = c("a", "b", "c"), n = 2L, labels = list(individual = c("a", "b", "c")) )
The point add_point() appends, on its own: one row per position of
every other identity variable, derived from the members of the collapsed
one.
compute_point( data, across = NULL, method = "centroid", include = NULL, exclude = NULL, name = NULL )compute_point( data, across = NULL, method = "centroid", include = NULL, exclude = NULL, name = NULL )
data |
An anipoint with Cartesian coordinates. |
across |
Identity variables to collapse — the dimensions the new point is derived over. Required when the frame declares more than one identity variable, since their order is not a hierarchy and there is no finest one to assume; with a single identity variable, that one is the default. Collapsing every level gives a single point per position. |
method |
How each coordinate is derived from the members' values:
Missing values are ignored; a moment where every member is missing gives
|
include, exclude
|
Values of the collapsed level to keep or leave out.
Only meaningful when one level is collapsed. A midpoint is the centroid
of two members: |
name |
Name for the new member. Defaults to |
An anipoint containing only the new member. Its confidence, if
the frame has one, is NA.
af <- anicore::example_anipoint(n_obs = 20, n_individuals = 2, n_keypoints = 3) # The centroid of each animal's keypoints compute_point(af, across = "keypoint") # Their median compute_point(af, across = "keypoint", method = "median")af <- anicore::example_anipoint(n_obs = 20, n_individuals = 2, n_keypoints = 3) # The centroid of each animal's keypoints compute_point(af, across = "keypoint") # Their median compute_point(af, across = "keypoint", method = "median")
Compute sinuosity index from precomputed vectors
compute_sinuosity( mean_step_length, mean_cos_turning, method = c("corrected", "original") )compute_sinuosity( mean_step_length, mean_cos_turning, method = c("corrected", "original") )
mean_step_length |
Numeric vector of mean step lengths within window |
mean_cos_turning |
Numeric vector of mean cosine of turning angles |
method |
Either "corrected" (Benhamou 2004) or "original" (Bovet & Benhamou 1988) |
Numeric vector of sinuosity values
compute_sinuosity(mean_step_length = 1.2, mean_cos_turning = 0.8) # The original formulation, for comparison with older work compute_sinuosity(1.2, 0.8, method = "original")compute_sinuosity(mean_step_length = 1.2, mean_cos_turning = 0.8) # The original formulation, for comparison with older work compute_sinuosity(1.2, 0.8, method = "original")
Compute straightness index from precomputed vectors
compute_straightness(displacement, path_length)compute_straightness(displacement, path_length)
displacement |
Numeric vector of net displacements (D) |
path_length |
Numeric vector of path lengths (L) |
Numeric vector of straightness values (D/L)
# Straight-line displacement over the distance actually travelled compute_straightness(5, 10) # A perfectly straight path scores 1 compute_straightness(10, 10)# Straight-line displacement over the distance actually travelled compute_straightness(5, 10) # A perfectly straight path scores 1 compute_straightness(10, 10)
Wrapper around compute_gradient() that optionally applies the gradient
operator multiple times (order). If no explicit time vector is supplied,
a simple index sequence is used.
differentiate(x, time = NULL, order = 1)differentiate(x, time = NULL, order = 1)
x |
Numeric vector of observations. |
time |
Optional numeric vector of timestamps. Must be the same length
as |
order |
Integer ≥ 1 indicating how many times the differentiation should be applied. Defaults to a single derivative. |
The function computes the first-order derivative using the
Fornberg-based scheme implemented in compute_gradient(). When
order > 1, the gradient is applied iteratively to the result of the
previous iteration.
Numeric vector of the same length as x containing the
differentiated values.
Fornberg, B. (1988). Generation of finite difference formulas on arbitrarily spaced grids. Mathematics of Computation, 51(184), 699-706. doi:10.1090/S0025-5718-1988-0935077-0
# Simple equally spaced case y <- sin(seq(0, 2 * pi, length.out = 10)) differentiate(y) # Uneven time stamps t <- c(0, 0.9, 2.1, 3.8, 5.0, 5.2, 5.6, 6.7, 7.2, 8.9) differentiate(y, time = t, order = 2)# Simple equally spaced case y <- sin(seq(0, 2 * pi, length.out = 10)) differentiate(y) # Uneven time stamps t <- c(0, 0.9, 2.1, 3.8, 5.0, 5.2, 5.6, 6.7, 7.2, 8.9) differentiate(y, time = t, order = 2)
Summarises the per-row measures of a frame over each group: a measure of
central tendency and of dispersion for each, one row per group. It
describes the measures the frame already carries — the output of
add_kinematics(), add_tortuosity() or your own mutate() —
and computes no new ones. Any grouping is allowed, since a median of rows
means the same whether the rows are one keypoint's or a whole animal's.
Properties of a trajectory as a whole, such as how far it ended up from
where it started, are not a statistic of any column; see
summarise_path().
Each frame class has its own default set of measures:
speed, acceleration, turning_speed, turning_rate,
turning_acceleration, course_elevation, the windowed measures of
add_tortuosity() at every window width present (straightness_11,
sinuosity_11, e_max_11, ...), and confidence; circular
statistics for course and for a declared yaw, reported as
*_heading.
length and confidence.
angle, with circular statistics, and confidence.
Only the measures present are summarised. The windowed measures are
recognised by their names, straightness, sinuosity or e_max followed
by _ and a window width, so a frame with several widths has each
summarised. The unsuffixed straightness, sinuosity and emax are
summarised too. Velocity and acceleration components, course_unwrapped,
and the running totals cumulative_distance and cumulative_turning are
left out by default; summarise_path() reports the totals.
Angles are summarised with circular statistics (anicore::circ_mean() and
its siblings), so that the mean of 350 and 10 degrees is 0 rather than 180.
They are computed in radians and reported in the frame's unit_angle.
Only the angles animetric knows of are treated as circular; any other
column is summarised as a linear quantity.
summarise_aniframe(data, ...) summarize_aniframe(data, ...) ## S3 method for class 'anipoint' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anisegment' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anijoint' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anievent' summarise_aniframe(data, ...) ## Default S3 method: summarise_aniframe(data, ...)summarise_aniframe(data, ...) summarize_aniframe(data, ...) ## S3 method for class 'anipoint' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anisegment' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anijoint' summarise_aniframe( data, cols = NULL, measures = c("median_mad", "mean_sd"), ... ) ## S3 method for class 'anievent' summarise_aniframe(data, ...) ## Default S3 method: summarise_aniframe(data, ...)
data |
An anipoint, anisegment or anijoint. |
... |
Not used. |
cols |
Character vector of columns to summarise, in place of the class's default set. A known angle among them is still summarised with circular statistics. |
measures |
Measures of central tendency and dispersion:
|
A data frame with one row per group: the grouping columns, then
<measure>_<column> for each measure, e.g. median_speed and
mad_speed.
summarise_path() for whole-trajectory measures.
kin <- add_kinematics( anicore::example_anipoint(n_obs = 20, n_individuals = 1, n_keypoints = 1) ) summarise_aniframe(kin) # Mean and standard deviation instead of median and MAD summarise_aniframe(kin, measures = "mean_sd") # Only some measures summarise_aniframe(kin, cols = c("speed", "course"))kin <- add_kinematics( anicore::example_anipoint(n_obs = 20, n_individuals = 1, n_keypoints = 1) ) summarise_aniframe(kin) # Mean and standard deviation instead of median and MAD summarise_aniframe(kin, measures = "mean_sd") # Only some measures summarise_aniframe(kin, cols = c("speed", "course"))
Measures of a path that are not a statistic of any per-row column: how long it was, where it ended up relative to where it started, and how directly it got there. Each needs one trajectory per group, since the distance between the ends of several pooled trajectories describes nothing.
Positions are all it needs: the kinematics are computed internally, so
add_kinematics() need not be run first. Any coordinate system
works; non-Cartesian frames are converted to Cartesian for the
computation.
For the distribution of per-row measures over each group, such as median
speed or the median of windowed straightness, see summarise_aniframe().
summarise_path(data, min_step = "auto", step_length = "auto") summarize_path(data, min_step = "auto", step_length = "auto")summarise_path(data, min_step = "auto", step_length = "auto") summarize_path(data, min_step = "auto", step_length = "auto")
data |
An anipoint, grouped one trajectory per group, as it is by its declared keys. |
min_step |
|
step_length |
|
Sinuosity is defined for a path of constant step length (Benhamou 2004),
so sinuosity and e_max come from the turning angles of the path
rediscretised to one: walking along the path, a new point is placed
wherever it first leaves a circle of that radius around the last one
(Bovet & Benhamou 1988). By default (step_length = "auto") the step
length is the trajectory's mean step between rows, weighted by step
length: the average step over the distance travelled, which time spent
still does not shorten. Sinuosity and E_max describe the path at the
scale of that step, and the automatic step differs between trajectories:
a faster animal gets a longer one. To compare trajectories, give them all
the same step_length. Tracking jitter that stays within the circle
while an animal is still gives no steps and no turning, where turning angles
between successive frames would be dominated by it. Missing positions
break the path into stretches that are rediscretised separately.
A data frame with one row per trajectory:
total_distance: distance travelled, the last value of
add_kinematics()'s cumulative_distance.
total_turning: turning of the direction of travel, summed (2D and
3D), in the frame's unit_angle: the last value of
add_kinematics()'s cumulative_turning, so steps shorter than
min_step add none.
net_displacement: straight-line distance from start to end.
straightness: net displacement over distance travelled, from 0 to
1.
sinuosity: corrected sinuosity index (Benhamou 2004), of the path
rediscretised to a constant step length (see Details).
e_max: maximum expected displacement (dimensionless), from the same
rediscretised path.
The windowed measures of add_tortuosity() carry the window width in
their names (straightness_11), so they never collide with these.
Benhamou, S. (2004). How to reliably estimate the tortuosity of an animal's path. Journal of Theoretical Biology, 229(2), 209-220. doi:10.1016/j.jtbi.2004.03.016.
Bovet, P., & Benhamou, S. (1988). Spatial analysis of animals' movements using a correlated random walk model. Journal of Theoretical Biology, 131(4), 419-433. doi:10.1016/S0022-5193(88)80038-9.
traj <- anicore::example_anipoint(n_obs = 20, n_individuals = 1, n_keypoints = 1) summarise_path(traj) # Count every direction toward total_turning, however short the step summarise_path(traj, min_step = 0) # Sinuosity at a step of your own, in the frame's spatial unit, the same # for every trajectory summarise_path(traj, step_length = 0.5)traj <- anicore::example_anipoint(n_obs = 20, n_individuals = 1, n_keypoints = 1) summarise_path(traj) # Count every direction toward total_turning, however short the step summarise_path(traj, min_step = 0) # Sinuosity at a step of your own, in the frame's spatial unit, the same # for every trajectory summarise_path(traj, step_length = 0.5)