tests/testthat/test-news.R

library(collapse)
library(xts)

test_that("news returns expected shapes", {
  set.seed(123)
  X <- matrix(rnorm(120), nrow = 24)
  colnames(X) <- paste0("v", seq_len(ncol(X)))
  X_old <- X
  X_new <- X
  X_old[20, 1] <- NA
  X_new[20, 1] <- 0.9
  X_old[18, 2] <- NA
  X_new[18, 2] <- X[18, 2]

  dfm_old <- DFM(X_old, r = 1, p = 1, em.method = "none")
  dfm_new <- DFM(X_new, r = 1, p = 1, em.method = "none")

  res <- news(dfm_old, dfm_new, t.fcst = 20, target.vars = 1)
  expect_s3_class(res, "dfm_news")

  expect_true(is.list(res))
  expect_true(is.data.frame(res$news_df))
  expect_equal(res$news_df$series, colnames(X))
  expect_true(is.numeric(res$y_old))
  expect_true(is.numeric(res$y_new))
})

test_that("news uses simple-case shortcut", {
  set.seed(42)
  X <- matrix(rnorm(80), nrow = 20)
  colnames(X) <- paste0("v", seq_len(ncol(X)))
  X_old <- X
  X_new <- X
  X_old[15, 2] <- NA
  X_new[15, 2] <- X[15, 2]

  dfm_old <- DFM(X_old, r = 1, p = 1, em.method = "none")
  dfm_new <- DFM(X_new, r = 1, p = 1, em.method = "none")

  res <- news(dfm_old, dfm_new, t.fcst = 10, target.vars = 1)

  res_data <- news(dfm_old, X_new, t.fcst = 10, target.vars = 1)
  expect_s3_class(res_data, "dfm_news")
  res_all <- news(dfm_old, X_new, t.fcst = 10)
  expect_s3_class(res_all, "dfm_news_list")
  expect_true(all(is.na(res_data$news_df$actual)))
  expect_true(all(is.na(res_data$news_df$forecast)))
  revision <- unname(res$y_new - res$y_old)
  sum_impact <- sum(res$news_df$impact)
  expect_equal(sum_impact, revision)
})

test_that("news works with MQ small model for monthly target", {
  skip_on_cran()

  # Construct BM14 database
  BM14 <- merge(BM14_M, BM14_Q)
  BM14[, BM14_Models$log_trans] <- log(BM14[, BM14_Models$log_trans])
  BM14[, BM14_Models$freq == "M"] <- fdiff(BM14[, BM14_Models$freq == "M"])
  BM14[, BM14_Models$freq == "Q"] <- fdiff(BM14[, BM14_Models$freq == "Q"], 3)

  # Small model data
  X_small <- qM(BM14[, BM14_Models$small])
  colnames(X_small) <- BM14_Models$series[BM14_Models$small]
  quarterly.vars <- BM14_Models$series[BM14_Models$small & BM14_Models$freq == "Q"]

  # Create vintages: X_old has fewer monthly observations (simulating older vintage)
  X_old <- X_small
  X_new <- X_small

  # Create releases from observed variables (new_cars, pms_pmi are observed near end)
  # X_old[355, "new_cars"] <- NA
  X_old[356, "new_cars"] <- NA
  X_old[357, "new_cars"] <- NA
  # X_old[354, "pms_pmi"] <- NA
  X_old[356, "pms_pmi"] <- NA
  X_old[357, "pms_pmi"] <- NA

  # Fit models with mixed-frequency
  dfm_old <- DFM(X_old, r = 2, p = 2, quarterly.vars = quarterly.vars)
  dfm_new <- DFM(X_new, r = 2, p = 2, quarterly.vars = quarterly.vars)

  # This is a proper nowcast scenario where target is NOT observed
  res_m <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "orders")

  expect_s3_class(res_m, "dfm_news")
  expect_true(is.numeric(res_m$y_old))
  expect_true(is.numeric(res_m$y_new))

  # For monthly targets, revision should equal sum of impacts
  revision_m <- unname(res_m$y_new - res_m$y_old)
  sum_impact <- sum(res_m$news_df$impact)
  expect_equal(revision_m, sum_impact, tolerance = 1e-8)

  # Standardized scale should match more tightly
  res_m_std <- news(dfm_old, dfm_new, t.fcst = 355, target.vars = "orders", standardized = TRUE)
  revision_m_std <- unname(res_m_std$y_new - res_m_std$y_old)
  sum_impact_std <- sum(res_m_std$news_df$impact)
  expect_lt(abs(revision_m_std - sum_impact_std), 1e-8)

  # Check that released variables contributed news
  idx_rel <- match(c("new_cars", "pms_pmi"), res_m$news_df$series)
  expect_true(all(res_m$news_df$news[idx_rel] != 0))

  # Check gains exist for released variables
  expect_true(any(res_m$news_df$gain != 0))

  # Check that gain produces results.
  rel_idx <- which(!is.na(res_m$news_df$actual))
  expect_equal(res_m$news_df$impact[rel_idx], res_m$news_df$news[rel_idx] * res_m$news_df$gain[rel_idx])

  # res_m$news_df |> na_omit() |> tfm(test1 = news - (actual - forecast), test2 = impact - news * gain)

})

test_that("news works with MQ small model for quarterly target", {
  skip_on_cran()

  # Construct BM14 database
  BM14 <- merge(BM14_M, BM14_Q)
  BM14[, BM14_Models$log_trans] <- log(BM14[, BM14_Models$log_trans])
  BM14[, BM14_Models$freq == "M"] <- fdiff(BM14[, BM14_Models$freq == "M"])
  BM14[, BM14_Models$freq == "Q"] <- fdiff(BM14[, BM14_Models$freq == "Q"], 3)

  # Small model data
  X_small <- qM(BM14[, BM14_Models$small])
  colnames(X_small) <- BM14_Models$series[BM14_Models$small]
  quarterly.vars <- BM14_Models$series[BM14_Models$small & BM14_Models$freq == "Q"]

  # Create vintages: X_old has fewer monthly observations (simulating older vintage)
  X_old <- X_small[-1, ]
  X_new <- X_small[-1, ]

  # Create releases from observed variables
  X_old[355, "ip_tot_cstr"] <- NA
  X_old[355, "new_cars"] <- NA
  X_old[356, "new_cars"] <- NA
  X_old[356, "pms_pmi"] <- NA
  X_old[356, "euro325"] <- NA
  X_old[356, "capacity"] <- NA

  # Fit models with mixed-frequency
  dfm_old <- DFM(X_old, r = 2, p = 2, quarterly.vars = quarterly.vars, max.missing = 1)
  dfm_new <- DFM(X_new, r = 2, p = 2, quarterly.vars = quarterly.vars, max.missing = 1)

  # This is a proper nowcast scenario where target is NOT observed
  res_m <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "gdp")

  expect_s3_class(res_m, "dfm_news")
  expect_true(is.numeric(res_m$y_old))
  expect_true(is.numeric(res_m$y_new))

  # revision should equal sum of impacts
  revision_m <- unname(res_m$y_new - res_m$y_old)
  sum_impact <- sum(res_m$news_df$impact)
  expect_equal(revision_m, sum_impact, tolerance = 1e-8)

  # Standardized scale should match more tightly
  res_m_std <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "gdp", standardized = TRUE)
  revision_m_std <- unname(res_m_std$y_new - res_m_std$y_old)
  sum_impact_std <- sum(res_m_std$news_df$impact)
  expect_lt(abs(revision_m_std - sum_impact_std), 1e-8)

  # Check that released variables contributed news
  idx_rel <- match(c("ip_tot_cstr", "new_cars", "pms_pmi", "euro325", "capacity"), res_m$news_df$series)
  expect_true(all(res_m$news_df$news[idx_rel] != 0))

  # Check gains exist for released variables
  expect_true(any(res_m$news_df$gain != 0))

  # # Check that gain produces results.
  rel_idx <- which(!is.na(res_m$news_df$actual))
  expect_equal(res_m$news_df$impact[rel_idx], res_m$news_df$news[rel_idx] * res_m$news_df$gain[rel_idx])

  # res_m$news_df |> na_omit() |> tfm(test1 = news - (actual - forecast), test2 = impact - news * gain)

})


test_that("news works with MQ medium model for monthly target", {
  skip_on_cran()

  # Construct BM14 database
  BM14 <- merge(BM14_M, BM14_Q)
  BM14[, BM14_Models$log_trans] <- log(BM14[, BM14_Models$log_trans])
  BM14[, BM14_Models$freq == "M"] <- fdiff(BM14[, BM14_Models$freq == "M"])
  BM14[, BM14_Models$freq == "Q"] <- fdiff(BM14[, BM14_Models$freq == "Q"], 3)

  # Medium model data
  X_medium <- qM(BM14[, BM14_Models$medium])
  colnames(X_medium) <- BM14_Models$series[BM14_Models$medium]
  quarterly.vars <- BM14_Models$series[BM14_Models$medium & BM14_Models$freq == "Q"]

  # Create vintages
  X_old <- X_medium
  X_new <- X_medium

  # Create releases from observed variables
  X_old[355, "new_cars"] <- NA
  X_old[356, "new_cars"] <- NA
  X_old[354, "pms_pmi"] <- NA
  X_old[355, "pms_pmi"] <- NA
  X_old[355, "ip_capital"] <- NA
  X_old[356, "us_ip"] <- NA

  # Fit models with mixed-frequency
  dfm_old <- DFM(X_old, r = 3, p = 2, quarterly.vars = quarterly.vars)
  dfm_new <- DFM(X_new, r = 3, p = 2, quarterly.vars = quarterly.vars)

  # Target: 'orders' naturally missing at end of sample
  res_m <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "orders")

  expect_s3_class(res_m, "dfm_news")
  expect_true(is.numeric(res_m$y_old))
  expect_true(is.numeric(res_m$y_new))

  # For monthly targets, revision should equal sum of impacts
  revision_m <- unname(res_m$y_new - res_m$y_old)
  sum_impact <- sum(res_m$news_df$impact)
  expect_equal(revision_m, sum_impact, tolerance = 1e-4)

  # Check that gain produces results.
  rel_idx <- which(!is.na(res_m$news_df$actual))
  expect_equal(res_m$news_df$impact[rel_idx], res_m$news_df$news[rel_idx] * res_m$news_df$gain[rel_idx])

  # Check gains exist for released variables
  expect_true(any(res_m$news_df$gain != 0))

  # res_m$news_df |> na_omit() |> tfm(test1 = news - (actual - forecast), test2 = impact - news * gain)

  # Standardized scale should match more tightly
  res_m_std <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "orders", standardized = TRUE)
  revision_m_std <- unname(res_m_std$y_new - res_m_std$y_old)
  sum_impact_std <- sum(res_m_std$news_df$impact)
  expect_lt(abs(revision_m_std - sum_impact_std), 1e-4)

  # Check that gain produces results on the standardized scale.
  rel_idx <- which(!is.na(res_m_std$news_df$actual))
  expect_equal(res_m_std$news_df$impact[rel_idx], res_m_std$news_df$news[rel_idx] * res_m_std$news_df$gain[rel_idx])

  expect_true(any(res_m_std$news_df$gain != 0))

  # res_m_std$news_df |> na_omit() |> tfm(test1 = news - (actual - forecast), test2 = impact - news * gain)

})

test_that("news works with idio.ar1 model", {
  set.seed(202)
  X <- matrix(rnorm(120), nrow = 24)
  colnames(X) <- paste0("v", seq_len(ncol(X)))
  X_old <- X
  X_new <- X
  X_old[20, 1] <- NA
  X_new[20, 1] <- 0.7

  dfm_old <- DFM(X_old, r = 1, p = 2, idio.ar1 = TRUE, em.method = "none")
  dfm_new <- DFM(X_new, r = 1, p = 2, idio.ar1 = TRUE, em.method = "none")

  res <- news(dfm_old, dfm_new, t.fcst = 20, target.vars = 1)
  expect_s3_class(res, "dfm_news")

  revision <- unname(res$y_new - res$y_old)
  sum_impact <- sum(res$news_df$impact)
  expect_equal(revision, sum_impact, tolerance = 1e-8)


  rel_idx <- which(!is.na(res$news_df$actual))
  expect_equal(res$news_df$impact[rel_idx], res$news_df$news[rel_idx] * res$news_df$gain[rel_idx])
})

test_that("news works with MQ + idio.ar1 model", {
  skip_on_cran()

  BM14 <- merge(BM14_M, BM14_Q)
  BM14[, BM14_Models$log_trans] <- log(BM14[, BM14_Models$log_trans])
  BM14[, BM14_Models$freq == "M"] <- fdiff(BM14[, BM14_Models$freq == "M"])
  BM14[, BM14_Models$freq == "Q"] <- fdiff(BM14[, BM14_Models$freq == "Q"], 3)

  X_small <- qM(BM14[, BM14_Models$small])
  colnames(X_small) <- BM14_Models$series[BM14_Models$small]
  quarterly.vars <- BM14_Models$series[BM14_Models$small & BM14_Models$freq == "Q"]

  X_old <- X_small[-1, ]
  X_new <- X_small[-1, ]

  # Create releases from observed variables
  X_old[355, "ip_tot_cstr"] <- NA
  X_old[355, "new_cars"] <- NA
  X_old[356, "new_cars"] <- NA
  X_old[356, "pms_pmi"] <- NA
  X_old[356, "euro325"] <- NA
  X_old[356, "capacity"] <- NA

  dfm_old <- DFM(X_old, r = 2, p = 2, quarterly.vars = quarterly.vars, idio.ar1 = TRUE, max.missing = 1)
  dfm_new <- DFM(X_new, r = 2, p = 2, quarterly.vars = quarterly.vars, idio.ar1 = TRUE, max.missing = 1)

  res_m <- news(dfm_old, dfm_new, t.fcst = 356, target.vars = "gdp")

  expect_s3_class(res_m, "dfm_news")
  revision_m <- unname(res_m$y_new - res_m$y_old)
  sum_impact <- sum(res_m$news_df$impact)
  expect_equal(revision_m, sum_impact, tolerance = 1e-6)

  rel_idx <- which(!is.na(res_m$news_df$actual))
  expect_equal(res_m$news_df$impact[rel_idx], res_m$news_df$news[rel_idx] * res_m$news_df$gain[rel_idx])

  # res_m_std$news_df |> na_omit() |> tfm(test1 = news - (actual - forecast), test2 = impact - news * gain)

})

test_that("news handles errors properly", {
  set.seed(101)
  X <- matrix(rnorm(80), nrow = 20)
  colnames(X) <- paste0("v", seq_len(ncol(X)))
  X_old <- X
  X_new <- X
  X_old[15, 2] <- NA
  X_new[15, 2] <- X[15, 2]

  dfm_old <- DFM(X_old, r = 1, p = 1, em.method = "none")

  expect_error(news(dfm_old, X_new, t.fcst = 25), "t.fcst is out of bounds")
  expect_error(news(dfm_old, X_new, t.fcst = 10, target.vars = "nonexistent"))
})

test_that("news_list works as expected", {
  set.seed(303)
  X <- matrix(rnorm(120), nrow = 24)
  colnames(X) <- paste0("v", seq_len(ncol(X)))
  X_old <- X
  X_new <- X
  X_old[20, 1] <- NA
  X_new[20, 1] <- 0.9
  X_old[18, 2] <- NA
  X_new[18, 2] <- X[18, 2]

  dfm_old <- DFM(X_old, r = 1, p = 1, em.method = "none")
  dfm_new <- DFM(X_new, r = 1, p = 1, em.method = "none")

  res_list <- news(dfm_old, dfm_new, t.fcst = 20)
  expect_s3_class(res_list, "dfm_news_list")
  expect_equal(length(res_list), ncol(X))
  expect_equal(names(res_list), colnames(X))

  # Test $ extraction
  res_v1 <- res_list$v1
  expect_s3_class(res_v1, "dfm_news")
  expect_equal(res_v1$target.var, attr(res_list, "target.vars")[1])
  expect_equal(res_v1$t.fcst, attr(res_list, "t.fcst"))
  expect_equal(res_v1$standardized, attr(res_list, "standardized"))
  expect_null(res_list$nonexistent)

  # Test [[ extraction by index
  res_idx <- res_list[[2]]
  expect_s3_class(res_idx, "dfm_news")
  expect_equal(res_idx$target.var, attr(res_list, "target.vars")[2])
  expect_equal(res_idx$t.fcst, attr(res_list, "t.fcst"))

  # Test [[ extraction by name
  res_name <- res_list[["v3"]]
  expect_s3_class(res_name, "dfm_news")
  expect_equal(res_name$target.var, attr(res_list, "target.vars")[3])
  expect_null(res_list[["nonexistent"]])

  # Test [ subsetting
  res_sub <- res_list[1:3]
  expect_s3_class(res_sub, "dfm_news_list")
  expect_equal(length(res_sub), 3)
  expect_equal(names(res_sub), c("v1", "v2", "v3"))
  expect_equal(attr(res_sub, "target.vars"), attr(res_list, "target.vars")[1:3])
  expect_equal(attr(res_sub, "t.fcst"), attr(res_list, "t.fcst"))

  # Test [ subsetting by name
  res_sub_name <- res_list[c("v2", "v4")]
  expect_s3_class(res_sub_name, "dfm_news_list")
  expect_equal(names(res_sub_name), c("v2", "v4"))
  expect_equal(attr(res_sub_name, "target.vars"), attr(res_list, "target.vars")[c(2, 4)])

  # Test as.data.frame
  df <- as.data.frame(res_list)
  expect_s3_class(df, "data.frame")
  expect_true("target" %in% names(df))
  expect_equal(attr(df, "target.vars"), attr(res_list, "target.vars"))
  expect_equal(attr(df, "t.fcst"), attr(res_list, "t.fcst"))
  expect_equal(attr(df, "standardized"), attr(res_list, "standardized"))
})

Try the dfms package in your browser

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

dfms documentation built on June 19, 2026, 1:06 a.m.