“Classical” statistics (and your intro courses) were built around controlled experiments where the Inference relies on models where the Response is:
But Movement Data is actually
one-dimensional: Multi-Dimensional! (X,Y,Time) > Solution: get comfortable with complex numbers.
independent: Highly dependent! > Solution: get comfortable with correlated / dependent data .
identically distributed!: Complex and heterogeneous! > Solution: behavioral change point analysis / segmentation, hidden markov models.
often normal: Circular and skewed distributions! > Solution: Flexible Likelihood-based modelling,
randomly/uniformly sampled: Gappy / irregular / oddly duty-cycled data > Solution: Continuous movement modeling.
To deal with these, complex numbers are a great option.
Complex numbers are a handy bookkeeping tool to package together two dimensions in a single quantity. They expand the one-dimensional (“Real”) number line into a second dimension (“Imaginary”).IMHO, they are the single most efficient way to deal with 2D vectors, both in math notation and in R code.
A complex number can be expressed as:
\[ Z = X + iY \]
where \(X\) is the real part and \(Y\) is the imaginary part.
The same number can be written as:
\[ Z = R \exp(i \theta) \]
where \(R\) is the length of the vector, also known as the modulus and \(\theta\) which indicates the orientation of the vector: the argument.
Let’s assume we have these three pairs of xy coordinates.
X <- c(3,4,-2)
Y <- c(0,3,2)
We can calculate complex numbers:
Z <- X + 1i*Y
Alternatively:
Z <- complex(re = X, im=Y)
We can plot Z
plot(Z, pch=19, col=1:3, asp=1)
arrows(rep(0,length(Z)), rep(0,length(Z)), Re(Z), Im(Z), lwd=2, col=1:3)
Note: ALWAYS use asp=1
- “aspect ratio = 1:1” - when plotting (properly projected) movement data!
Obtain length of vectors (the modulus):
Mod(Z)
and the orientation of vectors (the argument):
Arg(Z)
Note, the orientations are in radians, i.e. range from \(0\) to \(2\pi\) going counter-clockwise from the \(x\)-axis. Compass directions go from 0 to 360 clockwise. So, to convert to compass directions:
90-(Arg(Z)*180)/pi
We can create a hypothetical correlated random walk:
X <- cumsum(arima.sim(n=100, model=list(ar=.7)))
Y <- cumsum(arima.sim(n=100, model=list(ar=.7)))
Z <- X + 1i*Y
plot(Z, type="o", asp=1)
We can estimate instant summary statistics of this trajectory. Let’s look at the average location:
mean(Z)
Step vectors:
dZ <- diff(Z)
plot(dZ, asp=1, type="n")
arrows(rep(0, length(dZ)), rep(0, length(dZ)), Re(dZ), Im(dZ), col=rgb(0,0,0,.5), lwd=2, length=0.1)
We can look at the distribution of step lengths
S <- Mod(dZ)
summary(S)
hist(S, col="grey", bor="darkgrey", freq=FALSE)
lines(density(S), col=2, lwd=2)
We can estimate absolute orientations:
Phi <- Arg(dZ)
hist(Phi, col="grey", bor="darkgrey", freq=FALSE, breaks=seq(-pi,pi,pi/3))
Turning angles:
Theta <- diff(Phi)
hist(Theta, col="grey", bor="darkgrey", freq=FALSE)
QUESTION: What is a problem with this histogram?
Angles are a wrapped continuous variable, i.e. \(180^o > 0^o = 360^o < 180^o\). The best way to visualize the distribution of wrapped variables is with Rose-Diagrams. An R package that deals with circular data is circular().
require(circular)
Theta <- as.circular(Theta)
Phi <- as.circular(Phi)
rose.diag(Phi, bins=16, col="grey", prop=2, main=expression(Phi))
rose.diag(Theta, bins=16, col="grey", prop=2, main=expression(Theta))
LAB EXERCISE
We can add and substract vectors:
\[ Z_1 = X_1 + i Y_1; Z_2 = X_2 + i Y_2\] \[ Z_1 + Z_2 = (X_1 + X_2) + i(Y_1 + Y_2)\]
Useful, e.g., for shifting locations:
plot(Z, asp=1, type="l", col="darkgrey", xlim=c(-2,2)*max(Mod(Z)))
lines(Z - mean(Z), col=2, lwd=2)
lines(Z + Z[length(Z)], col=3, lwd=2)
We cal also multiply these complex vectors:
\[ Z_1 = R_1 \exp(i \theta_1); Z_2 = R_2 \exp(i \theta_2)\] \[ Z_1 Z_2 = R_1 R_2 \exp(i (\theta_1 + \theta_2))\]
If \(\text{Mod}(Z_2) = 1\), multiplications rotates by \(\text{Arg}(Z_2)\)
Rot1 <- complex(mod=1, arg=pi/4)
Rot2 <- complex(mod=1, arg=-pi/4)
plot(Z, asp=1, type="l", col="darkgrey", lwd=3)
lines(Z*Rot1, col=2, lwd=2)
lines(Z*Rot2, col=3, lwd=2)
A colorful loop can be produced by rotating iteratively through the vector. Here’s the code:
require(gplots)
plot(Z, asp=1, type="n", xlim=c(-1,1)*max(Mod(Z)), ylim=c(-1,1)*max(Mod(Z)))
cols <- rich.colors(1000,alpha=0.1)
thetas <- seq(0,2*pi,length=100)
for(i in 1:1000)
lines(Z*complex(mod=1, arg=thetas[i]), col=cols[i], lwd=4)
And here’s the plot:
I know you’re thinking …
Thanks for teaching me how to make a weird swirling rainbow thing … but why in the world would I want to shift and rotate my precious, precious data, which was just perfect the way it was?
My response: Null Sets for Pseudo Absences!
This is an in-depth summer predation study trying to answer questions related to habitat use which considered these two aspects:
Z <- Z[1:10]
n <- length(Z)
S <- Mod(diff(Z))
Phi <- Arg(diff(Z))
Theta <- diff(Phi)
RelSteps <- complex(mod = S[-1], arg=Theta)
Z0 <- Z[-((n-1):n)]
Z1 <- Z[-c(1,n)]
Z2 <- Z[-(1:2)]
Rotate <- complex(mod = 1, arg=Arg(Z1-Z0))
plot(c(0,RelSteps), asp=1, xlab="x", ylab="y", pch=19)
arrows(rep(0,n-2), rep(0, n-2), Re(RelSteps), Im(RelSteps), col="darkgrey")
Note: in practice (i.e. with tons of data), it is sufficient to randomly sample some smaller number (e.g. 30) null steps at each location.
Z.null <- matrix(0, ncol=n-2, nrow=n-2)
for(i in 1:length(Z1))
Z.null[i,] <- Z1[i] + RelSteps * Rotate[i]
palette(rich.colors(10))
plot(Z, type="o", col=1:10, pch=19, asp=1)
for(i in 1:nrow(Z.null))
segments(rep(Re(Z1[i]), n-2), rep(Im(Z1[i]), n-2), s
Re(Z.null[i,]), Im(Z.null[i,]), col=i+1)
The use of the null set is a way to test a narrower null hypothesis that accounts for auto correlation in the data.
The places the animal COULD HAVE but DID NOT go to are pseudo-absences, against which you can fit, e.g., logistic regression models (aka Step-selection functions).
Or just be simple/lazy (like us) and compare observed locations with Chi-squared tests:
Use (a portion) of the data you analyzed before.
# get pieces
n <- length(Z)
S <- Mod(diff(Z))
Phi <- Arg(diff(Z))
Theta <- diff(Phi)
RelSteps <- complex(mod = S[-1], arg=Theta)
# calculate null set
Z0 <- Z[-((n-1):n)]
Z1 <- Z[-c(1,n)]
Z.null <- matrix(0, ncol=n-2, nrow=n-2)
for(i in 1:length(Z1))
Z.null[i,] <- Z1[i] + sample(max(length(RelSteps),30)) * Rotate[i]
# plot
plot(Z, type="o", col=1:10, pch=19, asp=1)
for(i in 1:nrow(Z.null))
segments(rep(Re(Z1[i]), n-2), rep(Im(Z1[i]), n-2),
Re(Z.null[i,]), Im(Z.null[i,]), col=i+1)
Fuzzy Polar Bear Catterpillar!