Skip to content

Commit 2d79523

Browse files
authored
Add files via upload
1 parent dac0507 commit 2d79523

5 files changed

+253
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Code for preprocessing ANIMOV1 simulation data.
2+
3+
4+
df_DARs_synth <- read.csv(file=".../2KernelMovement.csv", skip=6) # first few rows have explanatory text, hence skipped.
5+
df_DARs_synth <- na.omit(df_DARs_synth)
6+
7+
8+
# To bring time in order
9+
10+
df_DARs_synth <- transform(df_DARs_synth, Delta=Delta+99*(Day-1)) # dataframe for synthetic (ANIMOVER1) data; time parameter in ANIMOV1 resets at the turn of each day.
11+
12+
13+
# To calculate turning angle given angle of heading.
14+
15+
colnames(df_DARs_synth)[6] <- "Heading" # Changing name of column 6 to reflect angle of heading.
16+
df_DARs_synth$Heading <- with(df_DARs_synth, pmin(df_DARs_synth$Heading, 360-df_DARs_synth$Heading))
17+
18+
Turning_angle_deg <- with(df_DARs_synth, diff(df_DARs_synth$Heading)) # Turning angle
19+
Turning_angle_deg <- abs(Turning_angle_deg) # To ensure turning angle is in (0,pi]
20+
df_DARs_synth <- df_DARs_synth[-1, ] # time parameters in the ANIMOVER\_1 RAMP to produce $99$ points # for each of our "nominal days" other than for the first, which has $100$ points.
21+
df_DARs_synth$Heading <- Turning_angle_deg
22+
rm(Turning_angle_deg)
23+
colnames(df_DARs_synth)[6] <- "Turning_angle_deg"
24+
df_DARs_synth <- transform(df_DARs_synth, Turning_angle_deg=Turning_angle_deg*pi/180)
25+
colnames(df_DARs_synth)[6] <- "Turning_angle_rad"
26+
27+
28+
# Renaming variables
29+
30+
df_DARs_synth <- df_DARs_synth[, c('X', 'Y', 'Delta', 'Distance', 'Turning_angle_rad')]
31+
colnames(df_DARs_synth)[1] <- "x"
32+
colnames(df_DARs_synth)[2] <- "y"
33+
colnames(df_DARs_synth)[3] <- "delta"
34+
colnames(df_DARs_synth)[4] <- "speed"
35+
colnames(df_DARs_synth)[5] <- "turning_angle"
36+
37+
38+
saveRDS(df_DARs_synth, file="merged-DARs_ANIMOV.Rds") # dataframe with step length and turning angle variables
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Code for preprocessing a single barn owl individual data.
2+
3+
4+
list_DARs_indiv <- readRDS(".../mid_to_mid1.Rds") # see https://github.com/LudovicaLV/DAR_project/blob/main/DAR1_measures.R for the code to extract DARs from multi-DAR time-series data of an individual.
5+
6+
df_DARs_indiv <- do.call(rbind, list_DARs_indiv) # working w/ all DARs of an individual
7+
8+
9+
# Calculate the variables for speed and turning angle, and append to the data frame.
10+
11+
df_DARs_indiv <- df_DARs_indiv[, c('X','Y','dateTime')]
12+
colnames(df_DARs_indiv)[1] <- "x"
13+
colnames(df_DARs_indiv)[2] <- "y"
14+
colnames(df_DARs_indiv)[3] <- "t"
15+
16+
df_DARs_indiv <- transform(df_DARs_indiv, T=as.numeric(T))
17+
speed <- vector("double", nrow(df_DARs_indiv))
18+
for (i in 2:nrow(df_DARs_indiv))
19+
{
20+
speed[i] <- sqrt(sum((df_DARs_indiv[i,] - df_DARs_indiv[i-1,])^2)
21+
- (df_DARs_indiv[i,3] - df_DARs_indiv[i-1,3])^2)/(df_DARs_indiv[i,3] - df_DARs_indiv[i-1,3])
22+
}
23+
df_DARs_indiv$speed <- speed
24+
df_DARs_indiv <- transform(df_DARs_indiv, T=as.POSIXct(T, origin="1970-01-01", tz="GMT"))
25+
26+
turning_angle <- vector("double", nrow(df_DARs_indiv))
27+
for (i in 3:nrow(df_DARs_indiv))
28+
{
29+
turning_angle[i] <- abs(atan((df_DARs_indiv[i,2]-df_DARs_indiv[i-1,2])/(df_DARs_indiv[i,1]-df_DARs_indiv[i-1,1]))
30+
- atan((df_DARs_indiv[i-1,2]-df_DARs_indiv[i-2,2])/(df_DARs_indiv[i-1,1]-df_DARs_indiv[i-2,1])))
31+
}
32+
df_DARs_indiv$turning_angle <- turning_angle
33+
34+
35+
df_DARs_indiv <- df_DARs_indiv[-1, ] # 1st point discarded for lack of turning angle value
36+
37+
38+
saveRDS(df_DARs_indiv, file="merged-DARs_barn-owl.Rds")
39+

02_segmentation.R

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Code for segmentation and clustering of either a barn owl individual or
2+
# ANIMOVER_1 synthetic data
3+
4+
5+
library(dplyr)
6+
library(tseries)
7+
8+
9+
df_DARs <- data.frame(readRDS(".../merged-DARs_ANIMOV.Rds")) # for ANIMOV data; line 9 must be uncommented and line 10 commented for ANIMOV data
10+
#df_DARs <- data.frame(readRDS(".../merged-DARs_barn-owl.Rds")) # for barn owl data; line 9 must be commented and line 10 uncommented for barn owl data
11+
12+
13+
df_DARs <- df_DARs[df_DARs_indiv$speed < 70,] # to remove obviously bogus points (unrealistically high barn owl speeds)
14+
15+
df_DARs$speed <- (df_DARs$speed)/max(df_DARs_indiv$speed)
16+
df_DARs$turning_angle <- (df_DARs$turning_angle)/pi
17+
# Scaling to bring the variables to range on [0,1].
18+
19+
20+
# Method to create segments and construct a representation.
21+
22+
create_chunks <- function(df)
23+
{
24+
df_chunks_DARs <- df |>
25+
group_by(seg=cut(T, breaks="60 sec")) |> # for barn owl; line 25 must be uncommented and line 26 commented for barn owl data
26+
#group_by(seg=cut(T, breaks=seq(0, max(T), by=30))) |> # for ANIMOV; line 25 must be commented and line 26 uncommented for ANIMOV data
27+
summarise(
28+
speed_mean = mean(speed, na.rm = TRUE),
29+
speed_std = sd(speed, na.rm = TRUE),
30+
turning_angle_mean = mean(turning_angle, na.rm = TRUE),
31+
turning_angle_std = sd(turning_angle, na.rm = TRUE),
32+
disp_ends = sqrt((x[n()] - x[1])^2 + (y[n()] - y[1])^2),
33+
disp_consec = sum(sqrt(diff(x)^2 + diff(y)^2)),
34+
n_pts = n(),
35+
fraction_WP = sum(grepl("^WP", Kernel))/n(), # Only for ANIMOV data; comment line 35 and comma at the end of line 34 for barn owl data
36+
) |>
37+
filter(n_pts >= 12) |> # Only for the case of barn owls
38+
summarise(
39+
seg,
40+
speed_mean, speed_std, turning_angle_mean, turning_angle_std,
41+
net_displacement = disp_ends/disp_consec,
42+
fraction_WP # Only for ANIMOV data; comment line 42 and comma at the end of line 41 for barn owl data
43+
)
44+
45+
df_chunks_DARs <- na.omit(df_chunks_DARs)
46+
47+
return(df_chunks_DARs)
48+
}
49+
50+
df_chunks_DARs = create_chunks(df_DARs)
51+
52+
53+
saveRDS(df_chunks_DARs, file="segmented-DARs.Rds")

03_clustering_visualisation.R

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
library(parallelDist)
2+
library(fastcluster)
3+
library(factoextra)
4+
5+
6+
df_chunks_DARs <- data.frame(readRDS(".../merged-DARs_ANIMOV.Rds"))
7+
8+
9+
# Hierarchical agglomerative clustering w/ Ward's criterion.
10+
11+
d <- parDist(as.matrix(df_chunks_DARs[c("speed_mean","turning_angle_mean","speed_std","turning_angle_std","net_displacement")]),
12+
method = "euclidean")
13+
hierarc_res <- fastcluster::hclust(d, method="ward.D2")
14+
15+
hc_ward_8 <- cutree(hierarc_res, k=8)
16+
17+
centroids <- NULL
18+
for(k in 1:8){
19+
centroids <-
20+
rbind(
21+
centroids,
22+
colMeans(df_chunks_DARs[c("speed_mean","turning_angle_mean","speed_std","turning_angle_std","net_displacement")][hc_ward_8==k, , drop = FALSE])
23+
)
24+
}
25+
print(noquote(paste0("Centroids of clusters: ",centroids)))
26+
27+
occupancy <-
28+
as.numeric(
29+
table(hc_ward_8)
30+
)
31+
print(noquote(paste0("Occupancy of clusters: ",occupancy)))
32+
33+
df_chunks_DARs_synth <- cbind(df_chunks_DARs_synth, clustNum=hc_ward_8)
34+
df_chunks_DARs_synth |>
35+
group_by(clustNum) |>
36+
summarise(
37+
Cluster_avg_fraction_WP_points_in_a_segment=mean(fraction_WP),
38+
Cluster_std_fraction_WP_points_in_a_segment=sd(fraction_WP),
39+
Cluster_std_net_displacement_of_a_segment=sd(net_displacement)
40+
) # lines 34 to 40 to be commented out for working with barn owl data
41+
42+
43+
# Compute principal components.
44+
45+
pca_res <-
46+
prcomp(
47+
df_chunks_DARs[, c("speed_mean","turning_angle_mean","speed_std","turning_angle_std","net_displacement")],
48+
scale=FALSE,
49+
center = FALSE
50+
)
51+
52+
# Eigenvalues
53+
get_eigenvalue(pca_res)
54+
55+
fviz_pca_var(pca_res,
56+
col.var = "contrib", # color by contributions to the PC
57+
repel = TRUE, # avoid text overlapping
58+
scale = FALSE,
59+
center = FALSE
60+
)
61+
62+
pca_res$x
63+
64+
# eigenvectors
65+
pca_res$rotation
66+
67+
68+
# Plot StaMEs in mean speed - mean turning angle space or PC1-PC2 space w/ different colours representing clusters.
69+
70+
cluster_data_PC <-
71+
data.frame(
72+
Cluster = as.factor(hc_ward_8),
73+
PC1 = pca_res$x[, 1],
74+
PC2 = pca_res$x[, 2]
75+
)
76+
var_expl <- round(pca_res$sdev^2 / sum(pca_res$sdev^2) * 100, 2)
77+
# Create a cluster plot
78+
ggplot(cluster_data_PC, aes(x = PC1, y = PC2, colour = Cluster)) +
79+
geom_point(size = 3) +
80+
scale_colour_manual(values = c("green1", "blue4", "lightblue1", "violet", "yellow1", "red1", "orange1", "red4")) +
81+
labs(title = "Cluster Plot on Principal Components",
82+
x = paste("PC1 (", var_expl[1], "%)", sep = ""),
83+
y = paste("PC2 (", var_expl[2], "%)", sep = "")) +
84+
theme_minimal()
85+
86+
cluster_data_SpTa <- data.frame(
87+
Cluster = as.factor(hc_ward_8),
88+
mean_speed = df_chunks_DARs_$speed_mean,
89+
mean_turning_angle = df_chunks_DARs$turning_angle_mean
90+
)
91+
# Create a cluster plot
92+
ggplot(
93+
cluster_data_SpTa,
94+
aes(x = mean_speed, y = mean_turning_angle, colour = Cluster)
95+
) +
96+
geom_point(size = 3) +
97+
#scale_color_manual(values = "red4")
98+
scale_color_manual(values = c("green1", "blue4", "lightblue1", "violet", "yellow1", "red1", "orange1", "red4")) +
99+
labs(title = "Cluster Plot on mean speed and mean turning angle") +
100+
theme_minimal()

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The Statistical Building Blocks of Animal Movement Simulations
2+
3+
4+
This repository shows a procedure to construct Statistical Movement Elements (StaMEs) from animal movement track. Movement data itself is in the form of a relocation time-sequence, given by a walk $`{W} = \{(t;x(t),y(t))|t=0,...,n^{time} \}.`$ The algorithm parses the normalised velocity (equivalent to normalised displacement or step length) and turning-angle time series (derived from the walk) into $z=1,\cdots,n^{\rm seg}$ segments (where $` n^{\rm seg}=\lfloor \frac{n^{\rm time}-1}{\mu} \rfloor `$) of size $\mu$ each. A clustering procedure is then applied on a hand-engineered representation comprising a set ($\cal S_{\mu}$) of the following statistical variables for each segment: mean velocity $V$, mean absolute turning angle $| \Delta \Theta |$, the associated standard deviations ${\rm SD}^{V}$ and ${\rm SD}^{| \Delta \Theta |}$, and finally, a normalized net displacement ($\Delta^{\rho}$). The last one is the Euclidean distance between the end points of each segment divided by a product of the number of points and mean step-length, and is required to pick up any possible circular motion type biases in movement.
5+
6+
Clustering is an unsupervised machine learning procedure which finds an optimum partition of a set of points with two or more variables into subsets (or clusters) of similar points. Here I've used hierarchical clustering approach, which results in a tree structure called dendrogram having a single cluster at the root, with leaf nodes representing data points in the set. Cluster analysis is performed on the segment level data (i.e., with $n_{\rm seg} = \lfloor \frac{t}{\mu} \rfloor +1$ points) using the variables in the se $\cal S_{\mu}$. Specifically, we want to find an optimal partition $\cal P(\cal S_{\mu}) = \{C_1, C_2, ..., C_k\},$ where $\cup_{i=1}^k C_i = \cal S_{\mu}$, and $C_i \cap C_j = \emptyset$ (hard clustering).
7+
8+
Hierarchical agglomerative algorithms implement bottom-up clustering methodology, which starts with each point of $\cal S$ in its own singleton cluster, followed by successive fusions of two clusters at a time, depending on similarity between them, leading to a specified number of clusters (or, alternatively, leading to one cluster followed by cutting the dendrogram at the desired number of clusters). These are deterministic, yet greedy, in the sense that the clusters are merged based entirely on the similarity measure, thereby yielding a local solution. Most of the similarity schemes are specified not in terms of an objective function to be optimized, but procedurally. I use Ward's minimum sum of square scheme, which performs a fusion of clusters while minimizing the intra-cluster variance. The distance metric to quantify dissimilarity is the Euclidean measure.
9+
10+
## Description of scripts
11+
12+
* 01_step-length_turning-angle_barn-owl.R: Computation of step length (or speed) and turning angle time series starting from the multi-DAR relocation time-series data for a barn owl individual obtained using the ATLAS reverse GPS system in north-eastern Israel.
13+
* 01_step-length_turning-angle_ANIMOVER1.R: Computation of step length and turning angle time series starting from 2-mode simulated multi-DAR relocation data generated from Numerus ANIMOVER_1.
14+
* 02_segmentation.R: Parsing of multi-DAR speed and turning angle series into segments of $`\mu`$ points each to construct a representation using a set of statistics for each segment for barn owl and ANIMOV data.
15+
* 03_clustering_visualisation.R: Clustering analysis on the representation previously obtained to perform StaME extraction along with visualisation of results for barn owl and ANIMOV data.
16+
17+
## Results
18+
19+
![ClustersOwl](https://github.com/Observarun/StaME-extraction_CAMs/assets/83636458/eb8c67a1-90ba-4407-8e61-866430789d92)
20+
*StaMEs obtained by clustering segments from the tracks of two different barn owls.*
21+
22+
![ClustersSim](https://github.com/Observarun/StaME-extraction_CAMs/assets/83636458/dfb5fef1-2af4-401e-b99d-c779e61516e7)
23+
*Clustering results from 10-point and 30-point segmentation of the simulation data.*

0 commit comments

Comments
 (0)