tests/testthat/test-woe.R

source(testthat::test_path("test-helpers.R"))

set.seed(1)
df <- data.frame(
  x1 = sample(c("A", "B", "C"), size = 20, replace = TRUE) %>% factor(),
  x2 = sample(c("A", "B", "C"), size = 20, replace = TRUE),
  stringsAsFactors = TRUE
) %>%
  mutate(y = rbinom(20, 1, prob = 1 / (1 + exp(-1 * (-4 + as.numeric(x1) + as.numeric(x2)))))) %>%
  mutate(y = if_else(y == 1, "A", "B"))

# woe_table ---------------------------------------------------------------

test_that("woe_table do not accept different length inputs", {
  expect_snapshot(
    error = TRUE,
    embed:::woe_table(rep(c(0, 1), 20), rep(letters[1:4], 5))
  )
})

test_that("woe_table accepts only outcome with 2 distinct categories", {
  expect_snapshot(
    error = TRUE,
    embed:::woe_table(rep(letters[1:3], 10), rep(c(0, 1, 2), 10))
  )
  expect_snapshot(
    error = TRUE,
    embed:::woe_table(rep(letters[1:3], 10), rep(c(0), 30))
  )
  expect_snapshot(
    error = TRUE,
    embed:::woe_table(df$x2, df$x1)
  )
})

test_that("woe_table returns a proper tibble", {
  expect_equal(dim(embed:::woe_table(df$x1, df$y)), c(3, 7))
  expect_identical(
    names(embed:::woe_table(df$x1, df$y)),
    c("predictor", "n_tot", "n_A", "n_B", "p_A", "p_B", "woe")
  )
})

test_that("logical outcome variables are treated properly", {
  expect_equal(
    dim(
      embed:::woe_table(c("A", "A", "A", "B"), c(TRUE, FALSE, TRUE, FALSE))
    ),
    c(2, 7)
  )
})

test_that("logical predictor variable are treated properly", {
  expect_equal(
    class(embed:::woe_table(c(TRUE, FALSE, TRUE, FALSE), c("A", "A", "A", "B"))$predictor),
    "character"
  )
})

test_that("woe_table ruturns no messages nor warnings", {
  expect_silent(embed:::woe_table(c(TRUE, FALSE, TRUE, FALSE), c("A", "A", "A", "B")))
  expect_silent(embed:::woe_table(c(TRUE, FALSE, TRUE, FALSE, NA), c("A", "A", "A", "B", "B")))
  expect_silent(embed:::woe_table(as.factor(c(TRUE, FALSE, TRUE, FALSE, NA)), c("A", "A", "A", "B", "B")))
  expect_silent(embed:::woe_table(df$x1, df$y))
})

test_that("Laplace works", {
  expect_true(all(is.finite(embed:::woe_table(c("A", "A", "B", "B"), c(0, 0, 0, 1), Laplace = 1e-6)$woe)))
  expect_false(all(is.finite(embed:::woe_table(c("A", "A", "B", "B"), c(0, 0, 0, 1), Laplace = 0)$woe)))
})

# dictionary --------------------------------------------------------------

test_that("dictionary returns a proper tibble", {
  expect_equal(dictionary(df, "y") %>% class(), c("tbl_df", "tbl", "data.frame"))
  expect_equal(dictionary(df, "y") %>% dim(), c(6, 9))
  expect_identical(
    dictionary(df, "y") %>% names(),
    c(
      "variable", "predictor", "n_tot", "n_A", "n_B",
      "p_A", "p_B", "woe", "outcome"
    )
  )
})

test_that("dictionary accepts numeric, logical and character predictor variables", {
  tmp <- mutate(df, x3 = rep(c(TRUE, FALSE), 10), x4 = rep(c(20, 30), 10))
  expect_equal(
    dim(dictionary(tmp, "y")),
    c(10, 9)
  )
})

test_that("dictionary returns no messages nor warnings nor errors", {
  expect_silent(dictionary(df, "y", x1))
  expect_silent(dictionary(df %>% mutate(x3 = rep(c(TRUE, FALSE), 10)), "y", x3))
})

# add_woe -----------------------------------------------------------------

test_that("add_woe returns a proper tibble", {
  expect_equal(add_woe(df, "y") %>% class(), c("tbl_df", "tbl", "data.frame"))
  expect_equal(add_woe(df, "y") %>% dim(), c(20, 5))
  expect_identical(add_woe(df, "y") %>% names(), c("x1", "x2", "y", "woe_x1", "woe_x2"))
})

test_that("add_woe accepts only outcome with 2 distinct categories", {
  expect_snapshot(error = TRUE, dictionary(df %>% filter(y %in% "B"), "y"))
})

test_that("add_woe ruturns no messages nor warnings nor errors", {
  expect_silent(add_woe(df, "y", x1))
  expect_silent(add_woe(df %>% mutate(x3 = rep(c(TRUE, FALSE), 10)), "y", x3))
})

test_that("add_woe accepts numeric, logical and character predictor variables", {
  expect_equal(
    add_woe(
      df %>%
        mutate(
          x3 = rep(c(TRUE, FALSE), 10),
          x4 = rep(c(20, 30), 10)
        ),
      "y"
    ) %>%
      dim(),
    c(20, 9)
  )
})

test_that("add_woe returns woe only for those variables that exists in both data and dictionary", {
  expect_equal(names(add_woe(df, "y", x2, dictionary = dictionary(df, "y", x1))), c("x1", "x2", "y"))
  expect_equal(names(add_woe(df, "y", x1, dictionary = dictionary(df, "y", x1))), c("x1", "x2", "y", "woe_x1"))
  expect_equal(names(add_woe(df, "y", dictionary = dictionary(df, "y", x1))), c("x1", "x2", "y", "woe_x1"))
  expect_equal(names(add_woe(df, "y", x1, x2, dictionary = dictionary(df, "y", x1))), c("x1", "x2", "y", "woe_x1"))
})

test_that("add_woe do not accept dictionary with unexpected layout", {
  expect_snapshot(
    error = TRUE,
    add_woe(df, outcome = "y", x1, dictionary = iris)
  )
  expect_snapshot(
    error = TRUE,
    add_woe(df, outcome = "y", x1, dictionary = iris %>% mutate(variable = 1))
  )
})

# test_that("add_woe warns user if the variable has too many levels", {
#   expect_warning(credit_data %>% add_woe("Status", Expenses))
# })

# step_woe ----------------------------------------------------------------

test_that("step_woe", {
  skip_if_not_installed("modeldata")
  data("credit_data", package = "modeldata")
  
  set.seed(342)
  in_training <- sample(seq_len(nrow(credit_data)), 2000)
  
  credit_tr <- credit_data[in_training, ]
  credit_te <- credit_data[-in_training, ]
  
  rec <-
    recipe(Status ~ ., data = credit_tr) %>%
    step_woe(Job, Home, outcome = vars(Status))

  expect_snapshot(
    woe_models <- prep(rec, training = credit_tr)
  )

  woe_dict <- credit_tr %>% dictionary("Status", Job, Home)
  expect_equal(woe_dict, woe_models$steps[[1]]$dictionary, ignore_attr = TRUE)

  bake_woe_output <- bake(woe_models, new_data = credit_te)
  add_woe_output <-
    credit_te %>%
    add_woe("Status", Job, Home, dictionary = woe_dict) %>%
    dplyr::select(one_of(names(bake_woe_output)))

  #
  expect_equal(bake_woe_output, add_woe_output)

  tidy_output <- tidy(woe_models, number = 1)
  woe_dict_output <-
    dictionary(credit_tr, Job, Home, outcome = "Status") %>%
    dplyr::rename(terms = variable, value = predictor)

  #
  expect_equal(tidy_output %>% dplyr::select(-id), woe_dict_output, ignore_attr = TRUE)

  rec_all_nominal <- recipe(Status ~ ., data = credit_tr) %>%
    step_woe(all_nominal(), outcome = vars(Status))

  #
  expect_snapshot(prep(rec_all_nominal, training = credit_tr, verbose = TRUE))

  rec_all_numeric <- recipe(Status ~ ., data = credit_tr) %>%
    step_woe(all_predictors(), outcome = vars(Status))

  #
  expect_snapshot(
    error = TRUE,
    prep(rec_all_numeric, training = credit_tr)
  )

  rec_discretize <- recipe(Status ~ ., data = credit_tr) %>% step_discretize(Price)
  rec_discretize_woe <- rec_discretize %>% step_woe(Price, outcome = vars(Status))

  prep_discretize <- prep(rec_discretize, training = credit_tr)
  prep_discretize_woe <- prep(rec_discretize_woe, training = credit_tr)

  bake_discretize <- bake(prep_discretize, new_data = credit_te)
  bake_discretize_woe <- bake(prep_discretize_woe, new_data = credit_te)

  expect_equal(
    sort(as.character(unique(bake_discretize$Price))),
    sort(prep_discretize_woe$steps[[2]]$dictionary$predictor)
  )
})

test_that("2-level factors", {
  iris3 <- iris
  iris3$group <- factor(rep(letters[1:5], each = 30))

  expect_snapshot(
    error = TRUE,
    recipe(Species ~ ., data = iris3) %>%
      step_woe(group, outcome = vars(Species)) %>%
      prep()
  )
})

test_that("woe_table respects factor levels", {
  dat <- tibble(
    predictor = sample(0:1, 100, TRUE),
    target = factor(predictor == 0, levels = c(TRUE, FALSE), labels = 0:1),
    target0 = relevel(target, ref = "0"),
    target1 = relevel(target, ref = "1")
  )

  expect_equal(
    woe_table(dat$predictor, dat$target0)$woe,
    -woe_table(dat$predictor, dat$target1)$woe
  )

  expect_identical(
    woe_table(dat$predictor, dat$target0) %>% select(-woe),
    woe_table(dat$predictor, dat$target1) %>% select(-woe)
  )
})

test_that("tunable", {
  rec <-
    recipe(~., data = mtcars) %>%
    step_woe(all_predictors(), outcome = "mpg")
  rec_param <- tunable.step_woe(rec$steps[[1]])
  expect_equal(rec_param$name, "Laplace")
  expect_true(all(rec_param$source == "recipe"))
  expect_true(is.list(rec_param$call_info))
  expect_equal(nrow(rec_param), 1)
  expect_equal(
    names(rec_param),
    c("name", "call_info", "source", "component", "component_id")
  )
})

# Infrastructure ---------------------------------------------------------------

test_that("bake method errors when needed non-standard role columns are missing", {
  skip_if_not_installed("modeldata")
  data("credit_data", package = "modeldata")

  rec <- recipe(credit_data) %>%
    step_woe(Job, Home, outcome = vars(Status)) %>%
    update_role(Job, new_role = "potato") %>%
    update_role_requirements(role = "potato", bake = FALSE)
  
  suppressWarnings(
    rec_trained <- prep(rec, training = credit_data, verbose = FALSE)
  )
  
  expect_error(
    bake(rec_trained, new_data = credit_data[, -8]),
    class = "new_data_missing_column"
  )
})

test_that("empty printing", {
  rec <- recipe(mpg ~ ., mtcars)
  rec <- step_woe(rec, outcome = vars(mpg))
  
  expect_snapshot(rec)
  
  rec <- prep(rec, mtcars)
  
  expect_snapshot(rec)
})

test_that("empty selection prep/bake is a no-op", {
  rec1 <- recipe(mpg ~ ., mtcars)
  rec2 <- step_woe(rec1, outcome = vars(mpg))
  
  rec1 <- prep(rec1, mtcars)
  rec2 <- prep(rec2, mtcars)
  
  baked1 <- bake(rec1, mtcars)
  baked2 <- bake(rec2, mtcars)
  
  expect_identical(baked1, baked2)
})

test_that("empty selection tidy method works", {
  rec <- recipe(mpg ~ ., mtcars)
  rec <- step_woe(rec, outcome = vars(mpg))
  
  expect <- res <- tibble(
    terms = character(),
    value = character(),
    n_tot = integer(),
    n_bad = integer(),
    n_good = integer(),
    p_bad = double(),
    p_good = double(),
    woe = double(),
    id = character()
  )
  
  expect_identical(tidy(rec, number = 1), expect)
  
  rec <- prep(rec, mtcars)
  
  expect_identical(tidy(rec, number = 1), expect)
})

test_that("keep_original_cols works", {
  skip_if_not_installed("modeldata")
  data("ames", package = "modeldata")
  
  new_names <- c("Street", "woe_Alley")
  
  rec <- recipe(Street ~ Alley, data = ames) %>%
    step_woe(Alley, outcome = vars(Street), keep_original_cols = FALSE)

  rec <- prep(rec)
  res <- bake(rec, new_data = NULL)
  
  expect_equal(
    colnames(res),
    new_names
  )
  
  rec <- recipe(Street ~ Alley, data = ames) %>%
    step_woe(Alley, outcome = vars(Street), keep_original_cols = TRUE)
  
  rec <- prep(rec)
  res <- bake(rec, new_data = NULL)
  
  expect_equal(
    colnames(res),
    c("Alley", new_names)
  )
})

test_that("keep_original_cols - can prep recipes with it missing", {
  skip_if_not_installed("modeldata")
  data("ames", package = "modeldata")
  
  rec <- recipe(Street ~ Alley, data = ames) %>%
    step_woe(Alley, outcome = vars(Street), keep_original_cols = FALSE)
  
  rec$steps[[1]]$keep_original_cols <- NULL
  
  expect_snapshot(
    rec <- prep(rec)
  )
  
  expect_error(
    bake(rec, new_data = ames),
    NA
  )
})

test_that("printing", {
  skip_if_not_installed("modeldata")
  data("credit_data", package = "modeldata")
  
  rec <- recipe(Status ~ ., data = credit_data) %>%
    step_woe(Job, Home, outcome = vars(Status))
  
  expect_snapshot(print(rec))
  expect_snapshot(prep(rec))
})

test_that("tunable is setup to works with extract_parameter_set_dials", {
  skip_if_not_installed("dials")
  rec <- recipe(~., data = mtcars) %>%
    step_woe(
      all_predictors(),
      outcome = "mpg",
      Laplace = hardhat::tune()
    )
  
  params <- extract_parameter_set_dials(rec)
  
  expect_s3_class(params, "parameters")
  expect_identical(nrow(params), 1L)
})
topepo/embed documentation built on March 26, 2024, 4:11 a.m.