tests/testthat/test-normalization.R

## ---------------------------------------------------------
## Normalization regression test
## Verifies that sigma2 (est.fect) is consistent with and
## without normalize=TRUE (the sigma2 normalization bug fix)
## ---------------------------------------------------------

test_that("sigma2 is consistent with and without normalization (FE)", {

  skip_on_cran()

    set.seed(6001)

    N <- 30
    TT <- 15
    T0 <- 10
    Ntr <- 10
    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, 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
    )

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

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

    ## ATT should be essentially the same
    expect_equal(out_raw$att.avg, out_norm$att.avg, tolerance = 0.01,
        label = "ATT should match with/without normalization")

    ## sigma2.fect should be close (within 10% relative error)
    ## This is the regression test for the normalization bug fix
    if (!is.null(out_raw$sigma2.fect) && !is.null(out_norm$sigma2.fect)) {
        rel_diff <- abs(out_raw$sigma2.fect - out_norm$sigma2.fect) /
            max(abs(out_raw$sigma2.fect), 1e-10)
        expect_lt(rel_diff, 0.1,
            label = paste0("sigma2.fect relative difference = ",
                           round(rel_diff, 6),
                           " (raw=", round(out_raw$sigma2.fect, 4),
                           ", norm=", round(out_norm$sigma2.fect, 4), ")"))
    }
})

test_that("sigma2 is consistent with and without normalization (IFE)", {

  skip_on_cran()

    set.seed(6002)

    N <- 30
    TT <- 15
    T0 <- 10
    Ntr <- 10
    tau <- 2.0

    alpha_i <- rnorm(N, 0, 1)
    xi_t <- rnorm(TT, 0, 0.5)
    lambda_i <- rnorm(N, 0, 0.5)
    f_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] +
                lambda_i[i] * f_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
    )

    out_raw <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "ife",
        r = 1,
        CV = FALSE,
        force = "two-way",
        normalize = FALSE,
        se = FALSE,
        parallel = FALSE
    ))

    out_norm <- suppressWarnings(fect::fect(
        Y ~ D,
        data = simdf,
        index = c("id", "time"),
        method = "ife",
        r = 1,
        CV = FALSE,
        force = "two-way",
        normalize = TRUE,
        se = FALSE,
        parallel = FALSE
    ))

    ## ATT should be essentially the same
    expect_equal(out_raw$att.avg, out_norm$att.avg, tolerance = 0.05,
        label = "IFE ATT should match with/without normalization")

    ## sigma2.fect should be close
    if (!is.null(out_raw$sigma2.fect) && !is.null(out_norm$sigma2.fect)) {
        rel_diff <- abs(out_raw$sigma2.fect - out_norm$sigma2.fect) /
            max(abs(out_raw$sigma2.fect), 1e-10)
        expect_lt(rel_diff, 0.15,
            label = paste0("IFE sigma2.fect relative difference = ",
                           round(rel_diff, 6)))
    }
})

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.