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.
Two ways in
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 control
bearings <- data.frame(bearing = c(10, 20, 350, 5), site = c("a", "a", "b", "b"))
hf <- headings_frame(bearings, col = "bearing", units = "degrees",
angle_convention = "clock")
hf
#> # A tibble: 4 × 2
#> bearing site
#> <dbl> <chr>
#> 1 1.40 a
#> 2 1.22 a
#> 3 1.75 b
#> 4 1.48 bAngle conventions
- Internally every angle is in radians in the unit-circle convention: 0 points East and angles increase counter-clockwise. Headings derived from tracks lie in [0, 2*pi).
-
headings_frame()converts on the way in.unitshas 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). - Summary tables carry two columns.
mean_diris in radians, unit-circle convention.mean_dir_degis in the display convention set bycirc_display(), which by default is 0 = North, clockwise. It is therefore notmean_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.
Coordinates and calibration
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)withradiusis 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 = TRUErescales 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 withorigin/radius.
Map every column yourself
- Without a dialect,
read_tracks()keeps only the mapped columns and those named inkeep(a dialect keeps the columns it produces). Name every grouping column you will need later inkeep, then bring it onto the headings withderive_headings(carry = ). A group comparison on a column that was not kept fails with “column … not found”. - A name in
keeporcarrythat is not in the data is dropped without a message. Check the result has the columns you asked for. - A
carryname that is already a column of the result (id,time,heading) is an error: the carried values would overwrite the derived ones. - A role missing from
mappingis guessed from the column names. An unmapped column calledtheta,angle,phi,bearing,degordegreesbecomes the angle, read in radians unlessangle_unitsays otherwise; one calledw,weightorweightsbecomes the weight. Mapid,time,xandyexplicitly, and checkguess_columns()on the table before loading.
Time
-
time_typesays how the time column is expressed:"frames","seconds"or"posix". Prefer stating it to relying on"auto". -
time_type = "frames"requiresfps. The frame rate is stored on theTracksobject and used byelapsed_seconds()and the kinematics functions. - radiatR never assumes a frame rate. Supply
fps, or set it afterwards withset_frame_rate().
Failures are information
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_ambiguous_decimal |
a ; file’s sniffed decimal point is shown only outside
the role columns, which hold values like 1.234 (1.234 or
1234?); pass read_opts = list(decimal = ) (also raised when
no data row of the first 20 lines is whole) |
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. - When the same id appears in more than one file, the default
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).
Heading rules and attrition
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.
Tests
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-Williams-
test_uniformity()asks whether headings are uniform around the circle;test = "vtest"tests against an expected directionmu, in radians. -
test_mean_directions(),test_concentration()andtest_distributions()compare groups. - Pass
axial = TRUEfor bidirectional data (an axis, not a direction). - Monte Carlo and permutation p-values, and the bootstrap functions,
use R’s random number generator. Call
set.seed()first and report the seed.
Pitfalls
- Reporting
mean_dir_degas if it were counter-clockwise from East. - Treating
normalize_xy = TRUEas calibration. - Forgetting
keep/carry, or misspelling a name in them, then finding the grouping column is gone. - Using
on_invalid = "drop"without saying how many rows were dropped. - Ignoring
n_missingafterderive_headings(). - Giving radii to a heading rule in the wrong units.
- Reporting a resampling p-value without a seed.
- Catching an error and carrying on with a different method.
- Letting
read_tracks()guess an angle or weight column from its name.
