tests/testthat/test-potential-outcomes.R

test_that("custom po handler", {
  # draw POs for it without arguments
  my_po_function <- function(data) {
    data$Y_Z_0 <- with(data, .25 + extra)
    data$Y_Z_1 <- with(data, extra)
    data
  }

  ## debugonce(declare_potential_outcomes)
  my_po_custom <- declare_potential_outcomes(handler = my_po_function)

  rm(my_po_function)
  pop_custom <- my_po_custom(sleep)

  expect_equal(
    colnames(pop_custom),
    c("extra", "group", "ID", "Y_Z_0", "Y_Z_1")
  )
})

test_that("custom po handler with args", {
  ## draw POs for it with arguments

  my_po_function <- function(data, q) {
    data$Y_Z_0 <- with(data, q + extra)
    data$Y_Z_1 <- with(data, extra)
    data
  }

  ## debugonce(declare_potential_outcomes)
  my_po_custom <- declare_potential_outcomes(
    handler = my_po_function,
    q = 2
  )

  ## debugonce(my_po_custom)
  rm(my_po_function)
  pop_custom <- my_po_custom(sleep)

  expect_equal(
    colnames(pop_custom),
    c("extra", "group", "ID", "Y_Z_0", "Y_Z_1")
  )
  expect_equal(pop_custom$Y_Z_1[1] - pop_custom$Y_Z_0[1], -2)
})


test_that("PO as discrete variables works", {
  extra <- 1

  my_potential_outcomes <- declare_potential_outcomes(
    Y_Z_0 = extra,
    Y_Z_1 = extra + 5
  )

  expect_equal(
    colnames(my_potential_outcomes(sleep)),
    c("extra", "group", "ID", "Y_Z_0", "Y_Z_1")
  )
})


# to: remove this N after capturing pars in

test_that("PO as a formula works", {
  N <- 3
  data <- fabricate(N = N)

  my_potential_outcomes_explicit <-
    declare_potential_outcomes(
      formula = R ~ rbinom(n = N, size = 1, prob = 1)
    )

  my_potential_outcomes_implicit <-
    declare_potential_outcomes(R ~ rbinom(n = N, size = 1, prob = 1))

  expect_identical(
    my_potential_outcomes_explicit(data), #
    my_potential_outcomes_implicit(data) # OK
  )
})

set.seed(5)
my_population <- declare_model(
  villages = add_level(N = 3, elevation = rnorm(N)),
  citizens = add_level(N = 4, income = runif(N))
)

# levels approach no longer working
test_that("POs at a higher level", {
  library(dplyr)
  my_population <- declare_model(
    villages = add_level(N = 3, elevation = rnorm(N)),
    citizens = add_level(N = 4, income = runif(N))
  )

  pop <- my_population()

  # different ways of doing the same thing

  # with "level" argument in a "formula" version

  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      formula = Y_vil ~ elevation + 5 + 2 * Z,
      level = "villages"
    )
  my_potential_outcomes_formula(pop)

  # with "level" argument in a "discrete" version
  my_potential_outcomes_discrete <-
    declare_potential_outcomes(
      Y_vil_Z_0 = elevation + 5,
      Y_vil_Z_1 = elevation + 5 + 2,
      level = "villages"
    )

  expect_equal(
    my_potential_outcomes_discrete(pop) |> head(),
    structure(
      list(
        villages = c("1", "1", "1", "1", "2", "2"),
        elevation = c(
          -0.840855480786298,
          -0.840855480786298,
          -0.840855480786298,
          -0.840855480786298,
          1.38435934347858,
          1.38435934347858
        ),
        citizens = c("01", "02", "03", "04", "05", "06"),
        income = c(
          0.527959984261543,
          0.807935200864449,
          0.9565001251176,
          0.110453018685803,
          0.273284949595109,
          0.490513201802969
        ),
        Y_vil_Z_0 = c(
          4.1591445192137,
          4.1591445192137,
          4.1591445192137,
          4.1591445192137,
          6.38435934347858,
          6.38435934347858
        ),
        Y_vil_Z_1 = c(
          6.1591445192137,
          6.1591445192137,
          6.1591445192137,
          6.1591445192137,
          8.38435934347858,
          8.38435934347858
        )
      ),
      row.names = c(NA, 6L),
      class = "data.frame"
    )
  )
})

pop <- my_population()

test_that("pos at a higher level with dplyr", {
  skip_if_not_installed("dplyr")
  library(dplyr)

  # with custom function
  my_custom_PO <- function(data) {
    data |>
      group_by(villages) |>
      mutate(
        Y_vil_Z_0 = elevation + 5,
        Y_vil_Z_1 = elevation + 5 + 2
      )
  }

  my_custom_PO(pop)

  my_potential_outcomes <-
    declare_potential_outcomes(
      formula = Y_vil ~ elevation + 5 + 2 * Z
    )

  my_design <-
    declare_model(data = pop) +
    declare_step(group_by, villages) +
    my_potential_outcomes

  my_design <-
    declare_model(data = pop) +
    declare_step(group_by, villages) +
    my_potential_outcomes

  expect_equal(nrow(draw_data(my_design)), 12)
})


test_that("draw POs at a level using a variable from another level (now allowed)", {
  my_population <- declare_model(
    villages = add_level(N = 2, elevation = 1:2),
    citizens = add_level(N = 2, income = c(.1, .3))
  )

  pop <- my_population()

  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      formula = Y_vil ~ elevation + income + 5,
      level = "villages"
    )

  expect_equivalent(
    my_potential_outcomes_formula(pop)$Y_vil_Z_1,
    c(6.1, 6.3, 7.1, 7.3)
  )
})


test_that("Potential outcomes with multiple assignment variables", {
  extra = 2
  beta <- c(1, 3)

  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      formula = test ~ extra + as.vector((cbind(z1, z2) %*% beta)),
      conditions = list(z1 = 0:1, z2 = 1:2)
    )

  out <- my_potential_outcomes_formula(sleep)

  with(out, {
    expect_equal(extra + 3, test_z1_0_z2_1)
    expect_equal(extra + 4, test_z1_1_z2_1)
    expect_equal(extra + 6, test_z1_0_z2_2)
    expect_equal(extra + 7, test_z1_1_z2_2)
  })

  # Assignment variables handled as conditions
  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      formula = test ~ extra + z1 + z2,
      assignment_variables = c("z1", "z2")
    )
  out <- my_potential_outcomes_formula(sleep)

  expect_true(my_potential_outcomes_formula(sleep) |> ncol() == 7)

  # my_potential_outcomes_formula <-
  #   declare_potential_outcomes(
  #     formula = test ~ extra + as.vector((cbind(z1, z2) %*% beta)),
  #     assignment_variables = c("z1", "z2")
  #   )
  # out <- my_potential_outcomes_formula(sleep)
  # with(out, {
  #   expect_equal(extra, test_z1_0_z2_0)
  #   expect_equal(extra + 3, test_z1_0_z2_1)
  #   expect_equal(extra + 1, test_z1_1_z2_0)
  #   expect_equal(extra + 4, test_z1_1_z2_1)
  #
})


test_that("Restore existing variables to be unchanged", {
  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      formula = test ~ extra + group,
      conditions = list(group = 1:2)
    )
  expect_identical(
    my_potential_outcomes_formula(sleep)$group,
    sleep$group
  )
})


test_that("Binary Potential outcomes", {
  my_potential_outcomes_formula <-
    declare_potential_outcomes(
      Y ~ draw_binary(prob = plogis(1000 * Z + extra))
    )

  out <- my_potential_outcomes_formula(sleep)
  expect_true(all(out$Y_Z_1 == 1))
})


test_that("Multiple assignment variables in PO", {
  po <- declare_potential_outcomes(
    Y ~ Z1 + Z2,
    conditions = list(Z1 = 0:1, Z2 = 0:1)
  )
  df <- po(sleep)
  expect_true(all(
    c("Y_Z1_0_Z2_0", "Y_Z1_1_Z2_0", "Y_Z1_0_Z2_1", "Y_Z1_1_Z2_1") %in%
      names(df)
  ))
})


test_that("handler dispatches correctly", {
  expect_error(
    DeclareDesign:::potential_outcomes_handler(
      Y ~ Z1 + Z2,
      conditions = expand.grid(Z1 = 0:1, Z2 = 0:1),
      assignment_variables = c("Z1", "Z2"),
      data = sleep,
      level = NULL
    ),
    NA
  )

  po <-
    DeclareDesign:::potential_outcomes_handler(
      Y ~ Z1 + Z2,
      conditions = expand.grid(Z1 = 0:1, Z2 = 0:1),
      data = sleep
    )

  po2 <-
    DeclareDesign:::potential_outcomes_handler(
      NULL,
      Y_Z1_0_Z2_0 = 0,
      Y_Z1_0_Z2_1 = 1,
      Y_Z1_1_Z2_0 = 1,
      Y_Z1_1_Z2_1 = 2,
      data = sleep,
      level = NULL
    )

  expect_true(all(
    c("Y_Z1_0_Z2_0", "Y_Z1_1_Z2_0", "Y_Z1_0_Z2_1", "Y_Z1_1_Z2_1") %in% names(po)
  ))
  expect_true(all(
    c("Y_Z1_0_Z2_0", "Y_Z1_1_Z2_0", "Y_Z1_0_Z2_1", "Y_Z1_1_Z2_1") %in%
      names(po2)
  ))
})


# to do: make sure xx appears in listed parameters also
test_that("environments for potential outcomes", {
  xx <- 3
  n = 2
  design <- declare_model(N = n) +
    declare_potential_outcomes(
      Y ~ Z * 1 + W,
      conditions = list(Z = c(0, 1), W = c(0, xx))
    )

  rm(xx, n)

  expect_true(design |> draw_data() |> ncol() == 5)
  dots_2 <- attr(design[[2]], "dots")

  #  env <- environment(dots_2$formula)
  #  expect_true(get("xx", envir = env) == 3)

  env <- environment(dots_2$conditions)
  expect_true(get("xx", envir = env) == 3)

  DeclareDesign:::find_all_objects(design)
})


# These need to be outside test environment
outcome_means = 1:3

test_that("multiarm old syntax from Design Library", {
  outcome_sds = 1:3

  N = 3
  sd_i = 1

  design <- declare_model(
    N = N,
    u_1 = rnorm(N, 0, outcome_sds[1L]),
    u_2 = rnorm(N, 0, outcome_sds[2L]),
    u_3 = rnorm(N, 0, outcome_sds[3L]),
    u = rnorm(N) * sd_i
  ) +

    declare_potential_outcomes(
      formula = Y ~
        (outcome_means[1] + u_1) *
          (Z == "1") +
          (outcome_means[2] + u_2) * (Z == "2") +
          (outcome_means[3] + u_3) * (Z == "3") +
          u,
      conditions = c("1", "2", "3"),
      assignment_variables = Z
    )

  expect_true(ncol(draw_data(design)) == 8)
})


test_that("more old syntax", {
  my_potential_outcomes <- declare_potential_outcomes(
    formula = Y ~ .25 * Z + .01 * age * Z,
    conditions = 1:4
  )

  expect_true(ncol(my_potential_outcomes(data.frame(age = 1))) == 5)
})

Try the DeclareDesign package in your browser

Any scripts or data that you put into this service are public.

DeclareDesign documentation built on Nov. 5, 2025, 6:02 p.m.