This guide is written for an AI agent asked to analyse directional data with radiatR, and for the person checking its work. It states the conventions the package uses, how it fails, and the mistakes that are easy to make. Every code chunk below is run when the package is built.
Start from trajectories when you have tracking output (x/y positions over time) and need one heading per track, or from a table of angles when the headings have already been measured.
sim <- as.data.frame(simulate_tracks(seed = 1, n_points = 50))
path <- tempfile(fileext = ".csv")
write.csv(sim, path, row.names = FALSE)
ts <- read_tracks(
path,
mapping = list(id = "trial_id", time = "frame", x = "abs_x", y = "abs_y"),
time_type = "frames",
fps = 30,
keep = "condition"
)
hd <- derive_headings(ts, rule = "distal", carry = "condition")
head(hd, 3)
#> # A tibble: 3 × 4
#> id time heading condition
#> <chr> <dbl> <dbl> <chr>
#> 1 control_001 50 5.90 control
#> 2 control_002 50 5.80 control
#> 3 control_003 49 5.84 controlheadings_frame() converts on the way in.
units has no default.
angle_convention = "clock" means 0 = North, increasing
clockwise (compass bearings); "unit_circle" means the data
are already 0 = East, counter-clockwise. In the example above a bearing
of 10 degrees is stored as 1.396 radians.headings_frame() does not wrap values supplied in the
unit-circle convention: 370 degrees is stored as 6.46 radians and -20
degrees as -0.35. Wrap them yourself if a later step needs [0,
2*pi).mean_dir is in
radians, unit-circle convention. mean_dir_deg is in the
display convention set by circ_display(),
which by default is 0 = North, clockwise. It is therefore not
mean_dir * 180 / pi.circ_summarise(hd, col = "heading", units = "radians", .by = "condition")
#> # A tibble: 3 × 6
#> condition n mean_dir mean_dir_deg resultant_R kappa
#> <chr> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 control 20 0.0125 89.3 0.929 7.31
#> 2 high 20 5.66 126. 0.670 1.83
#> 3 moderate 20 0.687 50.6 0.928 7.20State the convention whenever you report a direction.
Heading rules that use a radius (crossing,
exit, entry, ring_tangent,
distal with max_radius) read radii in the
units of the coordinates the Tracks object holds. Decide
how those coordinates relate to the arena before choosing a rule.
normalize_xy = FALSE (the default) keeps x/y as
supplied.origin = c(x, y) with radius is
calibration: every trajectory is mapped with
(xy - origin) / radius, so the arena edge is at radius 1
and bearings from the arena centre are preserved. Supply both
together.normalize_xy = TRUE rescales each trajectory on its own
bounding box. It preserves shape only and does not
preserve bearing from a fixed arena centre. It is not calibration and
cannot be combined with origin/radius.read_tracks() keeps only the mapped
columns and those named in keep (a dialect keeps the
columns it produces). Name every grouping column you will need later in
keep, then bring it onto the headings with
derive_headings(carry = ). A group comparison on a column
that was not kept fails with “column … not found”.keep or carry that is not in the
data is dropped without a message. Check the result has the columns you
asked for.carry name that is already a column of the result
(id, time, heading) is an error:
the carried values would overwrite the derived ones.mapping is guessed from the column
names. An unmapped column called theta, angle,
phi, bearing, deg or
degrees becomes the angle, read in radians unless
angle_unit says otherwise; one called w,
weight or weights becomes the weight. Map
id, time, x and y
explicitly, and check guess_columns() on the table before
loading.time_type says how the time column is expressed:
"frames", "seconds" or "posix".
Prefer stating it to relying on "auto".time_type = "frames" requires fps. The
frame rate is stored on the Tracks object and used by
elapsed_seconds() and the kinematics functions.fps, or set
it afterwards with set_frame_rate().radiatR stops rather than guessing. Read the error; do not work around it.
| Condition class | Meaning |
|---|---|
radiatR_invalid_rows |
x/y missing or non-finite; the error lists rows and ids |
radiatR_malformed_values |
a column holds values that are not numeric; lists rows and values,
and suggested_decimal when they look written with the other
decimal mark |
radiatR_invalid_tracks |
the data break a Tracks invariant (see
?tracks) |
radiatR_invalid_argument |
an argument cannot be used as given (for read_opts: an
unknown entry, a bad decimal mark, or an option the dialect cannot use;
option names it) |
radiatR_nonestimable |
a statistic cannot be estimated for a group |
radiatR_dropped_rows |
warning raised when on_invalid = "drop" removed
rows |
radiatR_attrition |
message from the deeplabcut, sleap,
ethovision and anymaze dialects: the points
masked at load and the rows left without a position (kept in
@meta$attrition; not the heading attrition of the next
section) |
radiatR_dialect_error |
a dialect cannot read the file as that program’s output; the message says why |
on_invalid = "error" is the default.
"drop" removes rows with missing or non-finite x/y and
warns; it is a decision about the data, so report it.id_collision = "error" stops. "namespace"
prefixes each id with its file’s name (less extension; the full name or
the end of the path where files share that).derive_headings() returns one row per track (some rules
can return more). When a rule yields no heading for a track, the row is
kept with NA and the result carries n_total
and n_missing (headings, i.e. rows), n_trials
(distinct trials) and missing_ids (each trial with a
missing heading, once) attributes. on_missing controls what
else happens: "warn" (the default) raises a warning,
"error" stops, "quiet" says nothing. The
ransac_straight rule draws random numbers, so call
set.seed() before it.
Attrition is often not random: a track that never reaches a ring has
no crossing heading, and those animals may differ from the
rest. Report n_missing alongside any statistic, and do not
switch rules silently to recover the missing tracks.
set.seed(1)
test_uniformity(hd, group_col = "condition", angle_col = "heading")
#> condition statistic p_value n test
#> 1 control 0.9289411 5.069033e-08 20 rayleigh
#> 2 high 0.6697242 4.300494e-05 20 rayleigh
#> 3 moderate 0.9277095 5.207452e-08 20 rayleigh
test_mean_directions(hd, group_col = "condition", angle_col = "heading")
#> n_groups statistic df1 df2 p_value test
#> 1 3 22.91549 2 57 4.976226e-08 Watson-Williamstest_uniformity() asks whether headings are uniform
around the circle; test = "vtest" tests against an expected
direction mu, in radians.test_mean_directions(),
test_concentration() and test_distributions()
compare groups.axial = TRUE for bidirectional data (an axis, not
a direction).set.seed() first and
report the seed.mean_dir_deg as if it were counter-clockwise
from East.normalize_xy = TRUE as calibration.keep / carry, or misspelling a
name in them, then finding the grouping column is gone.on_invalid = "drop" without saying how many rows
were dropped.n_missing after
derive_headings().read_tracks() guess an angle or weight column
from its name.