# REFERENCES
# http://www.stanford.edu/~hastie/Papers/biodiversity/Biodiversity.pdf
# Julia Viladomat, Rahul Mazumder, Alex McInturff, Douglas J. McCauley, 
# and Trevor Hastie (2013)
# Assessing the Significance of Global and Local Correlations under
# Spatial Autocorrelation; a Nonparametric Approach.


####### README
# The code for the algorithm proposed in the above paper contains two functions, 
# main and matching, with comments along the way to clarify the steps.
# At the end of this file we also include a small running example for illustration purposes.
#######


# Input parameters to main:

# data: data frame containing the sample (X, Y) (that one wishes to study 
# their correlation) and their coordinates for the N data locations.

# Delta: set of values for the proportion of neighbors to consider 
# for the smoothing step. No default. The user may have to experiment 
# with different values to find one suitable for their data.

# matching function.
matching <- function( X.randomized, long, lat, Delta, target_variog, prctile, ids, i ) {
  variog.X.delta <- vector(mode = "list", length = length(Delta))
  linear.fit <- variog.X.delta
  hat.X.delta <- variog.X.delta
  resid.sum.squares <- rep(0, length(Delta))
  for (k in 1:length(Delta)) { 
    # smooth X.randomized using locfit:
    fit <- locfit(X.randomized ~ lp(long, lat, nn = Delta[k], deg = 0), kern = "gauss", maxk = 300)
    X.delta <- fitted(fit) 
    # variogram of X.delta:
    variog.X.delta[[k]] <- variog(data = X.delta[ids], coords = cbind(long[ids],lat[ids]), option = "bin", max.dist = prctile, messages = FALSE)
    
    # linear regression between the target and X.delta variograms:
    linear.fit[[k]] <- lm(target_variog$v ~ 1 + variog.X.delta[[k]]$v)  
    # least square estimates:
    bet.hat <- as.numeric(linear.fit[[k]]$coefficients)
    # transformed X.delta:
    hat.X.delta[[k]] <- X.delta * sqrt(abs(bet.hat[2])) + rnorm(length(X.delta)) * sqrt(abs(bet.hat[1]))
    variog.hat.X.delta <- variog(data = hat.X.delta[[k]][ids], coords = cbind(long[ids],lat[ids]), option = "bin", max.dist = prctile, messages = FALSE)
    
    # sum of squares of the residuals:
    resid.sum.squares[k] <- sum((variog.hat.X.delta$v-target_variog$v) ^ 2)
  }
  # delta that minimizes the residual sum of squares:
  delta.star.id <- which.min(resid.sum.squares)
  hat.X.delta.star <- hat.X.delta[[delta.star.id]]
  return(list(residus = resid.sum.squares, delta.star.id = delta.star.id, hat.X.delta.star = hat.X.delta.star))
}

# main function.
main <- function(data, Delta) {
  
  library(geoR)
  library(locfit)
  library(foreach)
  library(doMC)
  no.wrks<-20
  registerDoMC(no.wrks-1)
  
  # load the data X, Y and the coordinates of the N data locations:
  X <- data[,1]
  Y <- data[,2]
  lat <- data[,3]
  long <- data[,4]
  N <- length(X)
  
  # If N is too big, we take a subsample of size N_s every time we calculate a variogram:
  N_s <- 1000
  if (length(X) > N_s) {
    ids <- sample(N, N_s)
    X_s <- X[ids]
    long_s <- long[ids]
    lat_s <- lat[ids]
  } else {
    X_s <- X
    long_s <- long
    lat_s <- lat
    ids <- 1:N
  }
  
  dists <- dist(cbind(lat_s, long_s))
  
  # maximum distance for the variogram set at the 25% percentile of 
  # the distribution of pairs of distances:
  prctile <- quantile(dists, probs = 0.25)
  
  # variogram of variable X that will be used as target when doing the matching:
  
  target_variog <- variog(data = X_s, coords = cbind(long_s,lat_s), max.dist = prctile, option = "bin", messages = FALSE)
  
  # ALGORITHM:
  # It returns B random fields with the same autocorrelation 
  # as X but independent of Y, stored in permutations. The basis
  # to calculate B realizations of the null we are interested in.
  
  B <- 1000
  permutations <- vector(mode = "list", length = B)
  permutations <- foreach(i = 1:B, .inorder = TRUE, .combine = cbind) %dopar% {
    print(c('i=', i))
    # random permutation of the values of X across locations:
    X.randomized <- sample(X, size = length(X), replace = FALSE)
    # smoothing and scaling step to match the target variogram:
    output <- matching(X.randomized, long, lat, Delta, target_variog, prctile, ids, i) 
    hat.X.delta.star <- output$hat.X.delta.star
    c(hat.X.delta.star)
  } 
  
  # ASSESSING THE SINGLE PEARSON'S CORRELATION COEFFICIENT (GLOBAL CORRELATION):
  
  # observed global correlation:
  cor.global.obs <- as.vector(cor(X,Y))
  
  # null distribution for the global correlation:
  cor.global <- cor(permutations,Y)
  
  extreme <- sum(abs(cor.global) > abs(cor.global.obs))
  # p-value:
  p.value.global <- extreme / B
  
  # ASSESSING THE CORRELATION BETWEEN X AND Y IN A GIVEN NEIGHBORHOOD (LOCAL CORRELATIONS)
  # whether to use a varying bandwidth to calculate the local correlations or not:
  
  varying.bandwidth = TRUE
  
  # observed local correlations calculated using locfit:
  XY <- X*Y
  X2 <- X^2
  Y2 <- Y^2
  if (varying.bandwidth) {
    # delta: proportion of neighbors (varying bandwitdh). It is set to 10% of N, but it may be changed by the user to find one more suitable for their data.
    delta <- 0.1
    fitX <- fitted(locfit(X ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
    fitY <- fitted(locfit(Y ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
    fitXY <- fitted(locfit(XY ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
    fitX2 <- fitted(locfit(X2 ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
    fitY2 <- fitted(locfit(Y2 ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
  } else {
    # lambda: non-varying bandwidth. The user may have to experiment with different values to find one suitable for their data.
    lambda <- 5.281
    fitX <- fitted(locfit(X ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
    fitY <- fitted(locfit(Y ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
    fitXY <- fitted(locfit(XY ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
    fitX2 <- fitted(locfit(X2 ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
    fitY2 <- fitted(locfit(Y2 ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300)) 
  }
  loc.cor.obs <- (fitXY - fitX * fitY) / (sqrt((fitX2 - fitX ^ 2) * (fitY2 - fitY ^ 2)))
  
  # loc.cor[i,] is the null distribution for the observed local correlation loc.cor.obs[i] at location i=1:N :
  loc.cor <- vector(mode = "list", length = B)
  loc.cor <- foreach(k = 1:B, .inorder = TRUE, .combine = cbind) %dopar% {
    Xper <- permutations[,k]
    XYper <- Xper*Y
    Xper2 <- Xper^2
    if (varying.bandwidth) {
      fitXper <- fitted(locfit(Xper ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
      fitXYper <- fitted(locfit(XYper ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
      fitXper2 <- fitted(locfit(Xper2 ~ lp(long, lat, nn = delta, deg = 0), kern = "gauss", maxk = 300))
    } else {
      fitXper <- fitted(locfit(Xper ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
      fitXYper <- fitted(locfit(XYper ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))
      fitXper2 <- fitted(locfit(Xper2 ~ lp(long, lat, h = lambda, deg = 0), kern = "gauss", maxk = 300))   
    }
    r_k <- (fitXYper - fitXper * fitY) / (sqrt((fitXper2 - fitXper ^ 2) * (fitY2 - fitY ^ 2)))
  }
  
  # p-value for each location i=1:N by counting the observations of the null distribution loc.cor[i,] more extreme than the observed loc.cor.obs[i]
  p.value.loc.cor <- rep(0,N)
  for (i in 1:N) {
    extreme <- sum(abs(loc.cor[i,]) > abs(loc.cor.obs[i]))
    p.value.loc.cor[i] <- extreme/B
  }
  return(list(permutations = permutations, p.value.global = p.value.global, p.value.loc.cor = p.value.loc.cor))
}

###########################

# The core of the main function is the algorithm proposed in the paper.
# It returns B=1000 random fields with the same autocorrelation as X 
# but independent of Y, stored in variable permutations. The algorithm 
# calls the matching function, which smoothes the permuted X to match
# the target variogram of X at every B.

# In addition to that, function main also calculates:

# 1. The null distribution for the Pearson's correlation coefficient 
# (global correlation) once the autocorrelation has been accounted for, 
# and the corresponding p-value to assess whether the observed 
# correlation coefficient between X and Y - cor(X,Y) - is significant.

# 2. A map of p-values, one for each location N, (and the nulls used to obtain them),
# to assess if the observed correlations between X and Y at a given neighbourhood
# (local correlations) are significant. The local correlations are calculated
# using the package locfit.

###########################


# RUNNING EXAMPLE -- Simulation of a Gaussian random field with 
#                      Gaussian autocorrelation function

library(RandomFields)

# grid
h <- 1
step <- 0.01 # resolution of the domain
x.grid <- c (0, h, step)

model <- "gauss"

# parameters
variance <- 1
scale <- 0.3 # smoothing parameter
mean <- 0
nugget <- 0

# Two independent realizations of the random field
f <- GaussRF(x = x.grid, y = x.grid, model = model, grid = T, gridtriple = T, param = c( mean, variance, nugget, scale ), n = 2)

n.grid <- seq (0, h, step)

X <- f[ , , 1]
Y <- f[ , , 2]
dim(X) <- c(length (n.grid) * length (n.grid), 1)
dim(Y) <- dim(X)
coords <- cbind(rep(n.grid, times = length (n.grid)), rep(n.grid, each = length(n.grid)))

# data to feed the algorithm with
data <- data.frame (X, Y, coords)
N <- 100
ids <- sample(nrow(data), N)
data <- data [ids, ] # small subsample

# call to main. The example takes ~ 20 secs on OS X 10.8.5 - 2.7 GHz Intel Core i7 - 16 GB 1600 MHz DDR3
# the following value for Delta works for the runing example. The user may have to experiment with different values to find one suitable for their data.

Delta <- seq(0.1,0.9,0.1)

results <- main(data = data , Delta = Delta)
