Stats we are used to can be summarized in plots like this:

“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

To deal with these, complex numbers are a great option.

Complex Numbers are Simple!

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.

An example

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!

Obtaining summary statistics

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

Lets play with a trajectory

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)

What about angles?

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?

Circular statistics

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

  1. Load movement data of choice!
  2. Convert the locations to a complex variable Z.
  3. Obtain a vector of time stamps T, draw a histogram of the time intervals. Then, ignore those differences.
  4. Obtain, summarize and illustrate:
  • the step lengths
  • the absolute orientation
  • the turning angles

Complex manipulations (are easy!)

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!

Example with Finnish Wolves

This is an in-depth summer predation study trying to answer questions related to habitat use which considered these two aspects:

Defining Null Sets

  1. Obtain all the steps and turning angles
  2. Rotate them by the orientation of the last step (\(Arg(Z_1-Z_0)\))
  3. Add the rotated steps to the last step (\(Z_1\))

Calculating Null Sets in R

  1. Obtain all the steps and turning angles
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)
  1. Rotate them by the orientation of the last step (\(Arg(Z_1-Z_0)\))
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.

  1. Add the rotated steps to the last step
Z.null <- matrix(0, ncol=n-2, nrow=n-2)
for(i in 1:length(Z1))
  Z.null[i,] <- Z1[i] + RelSteps * Rotate[i]
  1. Make the fuzzy catterpillar plot
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)

Using the null-set

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:

EXERCISE: Create a fuzzy-catterpillar plot!

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!