####################
# Author: James Hickey
#
# Series of tests for gbm loss functionality
#
####################

context("Error checking for GBM loss methods")
test_that("Error thrown responses not vector of doubles", {
  # Given offset, weights, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  offset <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  
  # When responses are not a vector of doubles
  resps <- rep("1", N)
  resps_2 <- NA
  resps_3 <- Inf
  resps_4 <- rep(NaN, N)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
  expect_error(loss(resps_2, preds, weights, offset, dist))
  expect_error(loss(resps_3, preds, weights, offset, dist))
  expect_error(loss(resps_4, preds, weights, offset, dist))
})
test_that("Error thrown when predictions not vector of doubles", {
  # Given offset, weights, responses and default dist
  N <- 100
  dist <- gbm_dist()
  offset <- runif(N)
  weights <- runif(N)
  resps <- runif(N)
  
  # When predictions are not a vector of doubles
  preds <- rep("1", N)
  preds_2 <- NA
  preds_3 <- Inf
  preds_4 <- rep(NaN, N)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
  expect_error(loss(resps, preds_2, weights, offset, dist))
  expect_error(loss(resps, preds_3, weights, offset, dist))
  expect_error(loss(resps, preds_4, weights, offset, dist))
})
test_that("Error thrown when weights not vector of doubles", {
  # Given offset, responses, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  offset <- runif(N)
  resps <- runif(N)
  preds <- runif(N)
  
  # When weights are not a vector of doubles
  weights <- rep("1", N)
  weights_2 <- NA
  weights_3 <- Inf
  weights_4 <- rep(NaN, N)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
  expect_error(loss(resps, preds, weights_2, offset, dist))
  expect_error(loss(resps, preds, weights_3, offset, dist))
  expect_error(loss(resps, preds, weights_4, offset, dist))
})
test_that("Error thrown when offset not vector of doubles", {
  # Given responses, weights, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  
  # When offsets are not a vector of doubles
  offset <- rep("1", N)
  offset_2 <- NA
  offset_3 <- Inf
  offset_4 <- rep(NaN, N)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
  expect_error(loss(resps, preds, weights, offset_2, dist))
  expect_error(loss(resps, preds, weights, offset_3, dist))
  expect_error(loss(resps, preds, weights, offset_4, dist))
})
test_that("Error thrown when baseline not vector of doubles", {
  # Given responses, weights, offset, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  
  # When baseline are not a vector of doubles
  baseline <- rep("1", N)
  baseline_2 <- NA
  baseline_3 <- Inf
  baseline_4 <- rep(NaN, N)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist, baseline))
  expect_error(loss(resps, preds, weights, offset, dist, baseline_2))
  expect_error(loss(resps, preds, weights, offset, dist, baseline_3))
  expect_error(loss(resps, preds, weights, offset, dist, baseline_4))
})
test_that("Error thrown when distribution is not a GBMDist", {
  # Given responses, weights, offset, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  
  # When dist obj is not GBMDist
  class(dist) <- ""
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when length of responses, predictions, weights and baseline not the same", {
  # Given responses, weights, offset, predictions and a baselines
  # of varying lengths
  N <- 100
  N2 <- 102
  dist <- gbm_dist()
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  
  resps_2 <- runif(N2)
  weights_2 <- runif(N2)
  preds_2 <- runif(N2)
  offset_2 <- runif(N2)
  baseline_2 <- runif(N2)
  
  # Then error thrown when trying to calculate the loss if 
  # length of responses, predictions, weights and baselines not the same
  expect_error(loss(resps_2, preds, weights, offset, dist, baseline))
  expect_error(loss(resps, preds_2, weights, offset, dist, baseline))
  expect_error(loss(resps, preds, weights_2, offset, dist, baseline))
  expect_error(loss(resps, preds_2, weights, offset_2, dist, baseline))
  expect_error(loss(resps, preds, weights, offset, dist, baseline_2))
})
test_that("Error thrown when length of offset is not the same as predictions", {
  # Given responses, weights, predictions and default dist
  N <- 100
  dist <- gbm_dist()
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  
  # When offset has different number of elements to preds
  offset <- runif(N+1)
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution object not recognised - default method", {
  # Given responses, weights, predictions and offset
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  
  # When distribution not recognised
  dist <- gbm_dist()
  class(dist) <- c("WeirdGBMDist", "GBMDist")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is CoxPH", {
  # Given responses, weights, predictions, offset and
  # CoxPH dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("CoxPH")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is Gamma", {
  # Given responses, weights, predictions, offset and
  # Gamma dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("Gamma")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is Huberized", {
  # Given responses, weights, predictions, offset and
  # Huberized dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("Huberized")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is Quantile", {
  # Given responses, weights, predictions, offset and
  # Quantile dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("Quantile")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is TDist", {
  # Given responses, weights, predictions, offset and
  # TDist dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("TDist")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})
test_that("Error thrown when distribution is Tweedie", {
  # Given responses, weights, predictions, offset and
  # Tweedie dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("Tweedie")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})

test_that("Error thrown if Pairwise but group_index is NULL", {
  # Given responses, weights, predictions, offset and
  # Pairwise dist but group_index not specified
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  dist <- gbm_dist("Pairwise", group="query")
  
  # Then error thrown when trying to calculate the loss
  expect_error(loss(resps, preds, weights, offset, dist))
})

context("Check loss calculation correct for various distributions")
test_that("Correctly calculates AdaBoost loss", {
  # Given responses, weights, predictions, offset, baseline and
  # AdaBoost dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  dist <- gbm_dist("AdaBoost")
  
  # When calculting loss
  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)
  
  # Then it is correct
  preds <- preds + offset
  loss_true <- weighted.mean(exp(-(2*resps-1)*preds), weights) - baseline
  expect_equal(calc_loss, loss_true)
})
test_that("Correctly calculates Bernoulli loss", {
  # Given responses, weights, predictions, offset, baseline and
  # Bernoulli dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  dist <- gbm_dist("Bernoulli")
  
  # When calculting loss
  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)
  
  # Then it is correct
  preds <- preds + offset
  loss_true <- -2*weighted.mean(resps*preds - log(1+exp(preds)), weights) - baseline
  expect_equal(calc_loss, loss_true)
})

test_that("Bernoulli loss is stable for large logits", {
  resps <- c(1, 0)
  preds <- c(1000, -1000)
  weights <- c(1, 1)
  offset <- c(0, 0)
  baseline <- c(0, 0)
  dist <- gbm_dist("Bernoulli")

  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)

  expect_true(all(is.finite(calc_loss)))
  expect_equal(calc_loss, c(0, 0))
})
test_that("Correctly calculates Gaussian loss", {
  # Given responses, weights, predictions, offset, baseline and
  # Gaussian dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  dist <- gbm_dist("Gaussian")
  
  # When calculting loss
  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)
  
  # Then it is correct
  preds <- preds + offset
  loss_true <- weighted.mean((resps - preds)^2, weights) - baseline
  expect_equal(calc_loss, loss_true)
})
test_that("Correctly calculates Laplace loss", {
  # Given responses, weights, predictions, offset, baseline and
  # Laplace dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  dist <- gbm_dist("Laplace")
  
  # When calculting loss
  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)
  
  # Then it is correct
  preds <- preds + offset
  loss_true <- weighted.mean(abs(resps-preds), weights) - baseline
  expect_equal(calc_loss, loss_true)
})

test_that("Correctly calculates Pairwise loss - ndcg", {
  skip("Skipping pairwise")
  # Given responses, weights, predictions, offset, baseline and
  # Given data and a fitted pairwise
  # create query groups, with an average size of 25 items each
  N <- 1000
  num.queries <- floor(N/25)
  query <- sample(1:num.queries, N, replace=TRUE)
  
  # X1 is a variable determined by query group only
  query.level <- runif(num.queries)
  X1 <- query.level[query]
  
  # X2 varies with each item
  X2 <- runif(N)
  
  # X3 is uncorrelated with target
  X3 <- runif(N)
  
  # The target
  Y <- X1 + X2
  
  # Add some random noise to X2 that is correlated with
  # queries, but uncorrelated with items
  
  X2 <- X2 + scale(runif(num.queries))[query]
  
  # Add some random noise to target
  SNR <- 5 # signal-to-noise ratio
  sigma <- sqrt(var(Y)/SNR)
  Y <- Y + runif(N, 0, sigma)
  
  data <- data.frame(Y, query=query, X1, X2, X3)
  dist <- gbm_dist("Pairwise", metric="ndcg", group="query")
  params <- training_params(num_trees = 2000, num_train = nrow(data),
                            id=seq_len(nrow(data)),
                            interaction_depth = 3)
  
  fit <- gbmt(Y~X1+X2+X3,          # formula
              data=data,     # dataset
              distribution=dist,
              train_params=params,
              keep_gbm_data=TRUE,      # store copy of input data in model
              cv_folds=5,          # number of cross validation folds
              is_verbose = FALSE ,    # don't print progress
              par_details=gbmParallel())  
  
  baseline <- runif(length(fit$fit))
  
  # When calculting loss
  set.seed(1)
  calc_loss <- loss(Y, fit$fit, fit$gbm_data_obj$weights, fit$gbm_data_obj$offset, fit$distribution, baseline)
  
  # Then it is correct
  preds <- fit$fit + fit$gbm_data_obj$offset
  set.seed(1)
  loss_true <- (1 - perf_pairwise(Y, preds, fit$distribution$group_index, dist$metric, 
                                  fit$gbm_data_obj$weights, fit$distribution$max_rank)) - baseline
  expect_equal(calc_loss, loss_true) # tolerance for random tie breaking
})

test_that("Correctly calculates Pairwise loss - conc", {
  skip("Skipping pairwise")
  # Given responses, weights, predictions, offset, baseline and
  # Given data and a fitted pairwise
  # create query groups, with an average size of 25 items each
  N <- 1000
  num.queries <- floor(N/25)
  query <- sample(1:num.queries, N, replace=TRUE)
  
  # X1 is a variable determined by query group only
  query.level <- runif(num.queries)
  X1 <- query.level[query]
  
  # X2 varies with each item
  X2 <- runif(N)
  
  # X3 is uncorrelated with target
  X3 <- runif(N)
  
  # The target
  Y <- X1 + X2
  
  # Add some random noise to X2 that is correlated with
  # queries, but uncorrelated with items
  
  X2 <- X2 + scale(runif(num.queries))[query]
  
  # Add some random noise to target
  SNR <- 5 # signal-to-noise ratio
  sigma <- sqrt(var(Y)/SNR)
  Y <- Y + runif(N, 0, sigma)
  
  data <- data.frame(Y, query=query, X1, X2, X3)
  dist <- gbm_dist("Pairwise", metric="conc", group="query")
  params <- training_params(num_trees = 2000, num_train = nrow(data), id=seq_len(nrow(data)),
                            interaction_depth = 3)
  
  fit <- gbmt(Y~X1+X2+X3,          # formula
              data=data,     # dataset
              distribution=dist,
              train_params=params,
              keep_gbm_data=TRUE,      # store copy of input data in model
              cv_folds=5,          # number of cross validation folds
              is_verbose = FALSE ,    # don't print progress
              par_details=gbmParallel())  
  
  baseline <- runif(length(fit$fit))
  
  # When calculting loss
  set.seed(1)
  calc_loss <- loss(Y, fit$fit, fit$gbm_data_obj$weights, fit$gbm_data_obj$offset, fit$distribution, baseline)
  
  # Then it is correct
  preds <- fit$fit + fit$gbm_data_obj$offset
  set.seed(1)
  loss_true <- (1 - perf_pairwise(Y, preds, fit$distribution$group_index, dist$metric, 
                                  fit$gbm_data_obj$weights, fit$distribution$max_rank)) - baseline
  expect_equal(calc_loss, loss_true) # tolerance for random tie breaking
})

test_that("Correctly calculates Pairwise loss - map", {
  skip("Skipping pairwise")
  # Given responses, weights, predictions, offset, baseline and
  # Given data and a fitted pairwise
  # create query groups, with an average size of 25 items each
  N <- 1000
  num.queries <- floor(N/25)
  query <- sample(1:num.queries, N, replace=TRUE)
  
  # X1 is a variable determined by query group only
  query.level <- runif(num.queries)
  X1 <- query.level[query]
  
  # X2 varies with each item
  X2 <- runif(N)
  
  # X3 is uncorrelated with target
  X3 <- runif(N)
  
  # The target
  Y <- X1 + X2
  
  # Add some random noise to X2 that is correlated with
  # queries, but uncorrelated with items
  
  X2 <- X2 + scale(runif(num.queries))[query]
  
  # Add some random noise to target
  SNR <- 5 # signal-to-noise ratio
  sigma <- sqrt(var(Y)/SNR)
  Y <- Y + runif(N, 0, sigma)
  Y[Y >= 1] <- 1
  Y[Y < 1] <- 0
  
  data <- data.frame(Y, query=query, X1, X2, X3)
  dist <- gbm_dist("Pairwise", metric="map", group="query")
  params <- training_params(num_trees = 2000, num_train = nrow(data),
                            id=seq_len(nrow(data)),
                            interaction_depth = 3)
  
  fit <- gbmt(Y~X1+X2+X3,          # formula
              data=data,     # dataset
              distribution=dist,
              train_params=params,
              keep_gbm_data=TRUE,      # store copy of input data in model
              cv_folds=5,          # number of cross validation folds
              is_verbose = FALSE ,    # don't print progress
              par_details=gbmParallel())  
  
  baseline <- runif(length(fit$fit))
  
  # When calculting loss
  set.seed(1)
  calc_loss <- loss(Y, fit$fit, fit$gbm_data_obj$weights, fit$gbm_data_obj$offset, fit$distribution, baseline)
  
  # Then it is correct
  preds <- fit$fit + fit$gbm_data_obj$offset
  set.seed(1)
  loss_true <- (1 - perf_pairwise(Y, preds, fit$distribution$group_index, dist$metric, 
                                  fit$gbm_data_obj$weights, fit$distribution$max_rank)) - baseline
  expect_equal(calc_loss, loss_true) # tolerance for random tie breaking
})

test_that("Correctly calculates Pairwise loss - mrr", {
  skip("Skipping pairwise")
  # Given responses, weights, predictions, offset, baseline and
  # Given data and a fitted pairwise
  # create query groups, with an average size of 25 items each
  N <- 1000
  num.queries <- floor(N/25)
  query <- sample(1:num.queries, N, replace=TRUE)
  
  # X1 is a variable determined by query group only
  query.level <- runif(num.queries)
  X1 <- query.level[query]
  
  # X2 varies with each item
  X2 <- runif(N)
  
  # X3 is uncorrelated with target
  X3 <- runif(N)
  
  # The target
  Y <- X1 + X2
  
  # Add some random noise to X2 that is correlated with
  # queries, but uncorrelated with items
  
  X2 <- X2 + scale(runif(num.queries))[query]
  
  # Add some random noise to target
  SNR <- 5 # signal-to-noise ratio
  sigma <- sqrt(var(Y)/SNR)
  Y <- Y + runif(N, 0, sigma)
  Y[Y >= 1] <- 1
  Y[Y < 1] <- 0
  
  data <- data.frame(Y, query=query, X1, X2, X3)
  dist <- gbm_dist("Pairwise", metric="mrr", group="query")
  params <- training_params(num_trees = 2000, num_train = nrow(data),
                            id=seq_len(nrow(data)),
                            interaction_depth = 3)
  
  fit <- gbmt(Y~X1+X2+X3,          # formula
              data=data,     # dataset
              distribution=dist,
              train_params=params,
              keep_gbm_data=TRUE,      # store copy of input data in model
              cv_folds=5,          # number of cross validation folds
              is_verbose = FALSE ,    # don't print progress
              par_details=gbmParallel())  
  
  baseline <- runif(length(fit$fit))
  
  # When calculting loss
  set.seed(1)
  calc_loss <- loss(Y, fit$fit, fit$gbm_data_obj$weights, fit$gbm_data_obj$offset, fit$distribution, baseline)
  
  # Then it is correct
  preds <- fit$fit + fit$gbm_data_obj$offset
  set.seed(1)
  loss_true <- (1 - perf_pairwise(Y, preds, fit$distribution$group_index, dist$metric, 
                                  fit$gbm_data_obj$weights, fit$distribution$max_rank)) - baseline
  expect_equal(calc_loss, loss_true) # tolerance for random tie breaking
})

test_that("Correctly calculates Poisson loss", {
  # Given responses, weights, predictions, offset, baseline and
  # AdaBoost dist
  N <- 100
  resps <- runif(N)
  weights <- runif(N)
  preds <- runif(N)
  offset <- runif(N)
  baseline <- runif(N)
  dist <- gbm_dist("Poisson")
  
  # When calculting loss
  calc_loss <- loss(resps, preds, weights, offset, dist, baseline)
  
  # Then it is correct
  preds <- preds + offset
  loss_true <- -2*weighted.mean(resps*preds-exp(preds), weights) - baseline
  expect_equal(calc_loss, loss_true)
})

