tests/testthat/test-edge-cases.R

## ---------------------------------------------------------
## Edge case and boundary condition tests
## ---------------------------------------------------------

test_that("fect handles single treated unit", {

  skip_on_cran()

    set.seed(7001)

    N <- 20
    TT <- 15
    T0 <- 10
    Ntr <- 1    # only one treated unit
    tau <- 3.0

    alpha_i <- rnorm(N, 0, 1)
    xi_t <- rnorm(TT, 0, 0.5)

    Y_vec <- numeric(N * TT)
    D_vec <- integer(N * TT)
    id_vec <- integer(N * TT)
    time_vec <- integer(N * TT)

    idx <- 1
    for (i in 1:N) {
        for (t in 1:TT) {
            treated <- (i <= Ntr) && (t > T0)
            D_vec[idx] <- as.integer(treated)
            eps <- rnorm(1, 0, 0.5)
            Y_vec[idx] <- alpha_i[i] + xi_t[t] + tau * D_vec[idx] + eps
            id_vec[idx] <- i
            time_vec[idx] <- t
            idx <- idx + 1
        }
    }

    simdf <- data.frame(
        id = id_vec,
        time = time_vec,
        Y = Y_vec,
        D = D_vec
    )

    ## Should not error with a single treated unit
    out <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "fe",
        force = "two-way",
        se = FALSE,
        parallel = FALSE
    ))

    expect_true(is.numeric(out$att.avg))
    expect_true(!is.na(out$att.avg))
    expect_true(is.matrix(out$eff))
})

test_that("fect handles missing data (scattered NAs in Y)", {

  skip_on_cran()

    set.seed(7002)

    N <- 25
    TT <- 15
    T0 <- 10
    Ntr <- 8
    tau <- 2.0

    alpha_i <- rnorm(N, 0, 1)
    xi_t <- rnorm(TT, 0, 0.5)

    Y_vec <- numeric(N * TT)
    D_vec <- integer(N * TT)
    id_vec <- integer(N * TT)
    time_vec <- integer(N * TT)

    idx <- 1
    for (i in 1:N) {
        for (t in 1:TT) {
            treated <- (i <= Ntr) && (t > T0)
            D_vec[idx] <- as.integer(treated)
            eps <- rnorm(1, 0, 1)
            Y_vec[idx] <- alpha_i[i] + xi_t[t] + tau * D_vec[idx] + eps
            id_vec[idx] <- i
            time_vec[idx] <- t
            idx <- idx + 1
        }
    }

    ## Introduce ~5% missing values in Y (but NOT in treated post-period)
    miss_candidates <- which(D_vec == 0)
    n_miss <- min(floor(0.05 * length(Y_vec)), length(miss_candidates))
    miss_idx <- sample(miss_candidates, n_miss)
    Y_vec[miss_idx] <- NA

    simdf <- data.frame(
        id = id_vec,
        time = time_vec,
        Y = Y_vec,
        D = D_vec
    )

    ## Should handle missing data without error
    out <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "fe",
        force = "two-way",
        se = FALSE,
        parallel = FALSE,
        na.rm = TRUE
    ))

    expect_true(is.numeric(out$att.avg))
    expect_true(!is.na(out$att.avg))
    ## ATT should still be roughly correct
    expect_lt(abs(out$att.avg - tau), 1.5,
        label = paste0("Missing data FE: att.avg = ",
                       round(out$att.avg, 4), ", tau = ", tau))
})

test_that("fect output structure is complete (FE)", {

  skip_on_cran()

    set.seed(7003)

    N <- 20
    TT <- 10
    T0 <- 6
    Ntr <- 8

    Y_vec <- numeric(N * TT)
    D_vec <- integer(N * TT)
    id_vec <- integer(N * TT)
    time_vec <- integer(N * TT)

    idx <- 1
    for (i in 1:N) {
        for (t in 1:TT) {
            treated <- (i <= Ntr) && (t > T0)
            D_vec[idx] <- as.integer(treated)
            Y_vec[idx] <- rnorm(1) + 2.0 * D_vec[idx]
            id_vec[idx] <- i
            time_vec[idx] <- t
            idx <- idx + 1
        }
    }

    simdf <- data.frame(
        id = id_vec,
        time = time_vec,
        Y = Y_vec,
        D = D_vec
    )

    out <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "fe",
        force = "two-way",
        se = FALSE,
        parallel = FALSE
    ))

    ## Check required output slots exist
    expect_true(!is.null(out$att.avg))
    expect_true(!is.null(out$att.avg.unit))
    expect_true(!is.null(out$eff))
    expect_true(!is.null(out$Y.ct))
    expect_true(!is.null(out$time))
    expect_true(!is.null(out$att))
    expect_true(!is.null(out$count))
    expect_true(!is.null(out$sigma2))
})

test_that("fect with many treated units still works", {

  skip_on_cran()

    set.seed(7004)

    N <- 25
    TT <- 12
    T0 <- 8
    Ntr <- 20  # 80% treated
    tau <- 1.5

    alpha_i <- rnorm(N, 0, 0.5)
    xi_t <- rnorm(TT, 0, 0.3)

    Y_vec <- numeric(N * TT)
    D_vec <- integer(N * TT)
    id_vec <- integer(N * TT)
    time_vec <- integer(N * TT)

    idx <- 1
    for (i in 1:N) {
        for (t in 1:TT) {
            treated <- (i <= Ntr) && (t > T0)
            D_vec[idx] <- as.integer(treated)
            eps <- rnorm(1, 0, 1)
            Y_vec[idx] <- alpha_i[i] + xi_t[t] + tau * D_vec[idx] + eps
            id_vec[idx] <- i
            time_vec[idx] <- t
            idx <- idx + 1
        }
    }

    simdf <- data.frame(
        id = id_vec,
        time = time_vec,
        Y = Y_vec,
        D = D_vec
    )

    ## Should work even with many treated / few control
    out <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "fe",
        force = "two-way",
        se = FALSE,
        parallel = FALSE
    ))

    expect_true(is.numeric(out$att.avg))
    expect_true(!is.na(out$att.avg))
})

Try the fect package in your browser

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

fect documentation built on April 30, 2026, 9:06 a.m.