tests/testthat/test-gkwreg-beta-vs-betareg.R

# ==============================================================================
# Test Suite: gkwreg (beta family) vs betareg Package
# Author: Lopes, J. E.
# Description: Rigorous testing of gkwreg's beta family implementation
#              against the reference betareg package, accounting for
#              parameterization differences
# ==============================================================================

library(testthat)
library(gkwreg)
library(gkwdist)
library(betareg)

# Setup helper function
setup_beta_comparison_data <- function(seed = 12345) {
  set.seed(seed)
  n <- 200
  x1 <- rnorm(n)
  x2 <- runif(n, -1, 1)

  # Generate using betareg parametrization
  eta_mu <- -0.5 + 0.8 * x1 - 0.4 * x2
  mu <- plogis(eta_mu)

  eta_phi <- 1.5 + 0.3 * x1
  phi <- exp(eta_phi)

  shape1 <- mu * phi
  shape2 <- (1 - mu) * phi
  y <- rbeta(n, shape1, shape2)
  y <- pmax(pmin(y, 1 - 1e-7), 1e-7)

  data.frame(y = y, x1 = x1, x2 = x2)
}

# ==============================================================================
# TEST 1: Density Function Equivalence
# ==============================================================================

test_that("Test 1: dbeta_ correctly implements Beta(gamma, delta+1)", {
  # Verify that dbeta_(x, gamma, delta) = dbeta(x, gamma, delta+1)

  x <- seq(0.01, 0.99, length.out = 100)
  gamma <- 2.5
  delta <- 3.2

  dens_gkw <- dbeta_(x, gamma, delta)
  dens_base <- dbeta(x, shape1 = gamma, shape2 = delta + 1)

  expect_equal(dens_gkw, dens_base, tolerance = 1e-14)
  expect_true(all(is.finite(dens_gkw)))
  expect_true(all(dens_gkw >= 0))
})

# ==============================================================================
# TEST 2: Log-Likelihood Calculation
# ==============================================================================

test_that("Test 2: Log-likelihood values should match for same distribution", {
  # When both models fit the same underlying Beta distribution,
  # log-likelihoods should be comparable (though not necessarily identical
  # due to different parametrizations)

  data <- setup_beta_comparison_data()

  # Fit simple intercept-only models
  fit_gkw <- gkwreg(y ~ 1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ 1, data = data)

  ll_gkw <- as.numeric(logLik(fit_gkw))
  ll_betareg <- as.numeric(logLik(fit_betareg))

  # Log-likelihoods should be finite
  expect_true(is.finite(ll_gkw))
  expect_true(is.finite(ll_betareg))

  # Relative difference should be small for intercept-only model
  rel_diff <- abs(ll_gkw - ll_betareg) / abs(ll_betareg)

  # This may fail - documenting the discrepancy
  if (rel_diff > 0.1) {
    warning(sprintf(
      "Log-likelihood difference detected: gkwreg = %.4f, betareg = %.4f, rel diff = %.2f%%",
      ll_gkw, ll_betareg, rel_diff * 100
    ))
  }
})

# ==============================================================================
# TEST 3: Fitted Values Correlation
# ==============================================================================

test_that("Test 3: Fitted values should be highly correlated", {
  # Even with different parametrizations, fitted means should match

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2, data = data)

  fitted_gkw <- fitted(fit_gkw)
  fitted_betareg <- fitted(fit_betareg)

  # Should be highly correlated
  correlation <- cor(fitted_gkw, fitted_betareg)
  expect_gt(correlation, 0.99)

  # Should be bounded in (0, 1)
  expect_true(all(fitted_gkw > 0 & fitted_gkw < 1))
  expect_true(all(fitted_betareg > 0 & fitted_betareg < 1))

  # Mean absolute difference should be small
  mad <- mean(abs(fitted_gkw - fitted_betareg))
  expect_lt(mad, 0.02) # Within 2%
})

# ==============================================================================
# TEST 4: Parameter Transformation Verification
# ==============================================================================

test_that("Test 4: Parameter transformations are consistent", {
  # Verify that gamma, delta from gkwreg relate correctly to
  # mu, phi from betareg

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1, data = data)

  # Extract parameters for first observation
  params_gkw <- predict(fit_gkw, type = "parameter")
  gamma <- as.numeric(params_gkw$gamma[1])
  delta <- as.numeric(params_gkw$delta[1])

  mu_betareg <- predict(fit_betareg, type = "response")[1]
  phi_betareg <- predict(fit_betareg, type = "precision")[1]

  # Verify relationships
  shape1_gkw <- gamma
  shape2_gkw <- delta + 1

  shape1_betareg <- mu_betareg * phi_betareg
  shape2_betareg <- (1 - mu_betareg) * phi_betareg

  # Calculate implied means
  mean_gkw <- as.numeric(gamma / (gamma + delta + 1))
  mean_betareg <- as.numeric(mu_betareg)

  # Means should be close
  expect_equal(mean_gkw, mean_betareg, tolerance = 0.05)

  # Shapes should be positive
  expect_gt(shape1_gkw, 0)
  expect_gt(shape2_gkw, 0)
  expect_gt(shape1_betareg, 0)
  expect_gt(shape2_betareg, 0)
})

# ==============================================================================
# TEST 5: Density Evaluation Consistency
# ==============================================================================

test_that("Test 5: Density evaluations should match at observed points", {
  # For same observation, both models should give similar densities

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1, data = data)

  # Test first 10 observations
  for (i in 1:10) {
    y_i <- data$y[i]

    # Get parameters from gkwreg
    params_gkw <- predict(fit_gkw, type = "parameter")
    gamma_i <- params_gkw$gamma[i]
    delta_i <- params_gkw$delta[i]

    # Get parameters from betareg
    mu_i <- predict(fit_betareg, type = "response")[i]
    phi_i <- predict(fit_betareg, type = "precision")[i]

    # Calculate densities
    dens_gkw <- dbeta_(y_i, gamma_i, delta_i)
    dens_betareg <- dbeta(y_i, mu_i * phi_i, (1 - mu_i) * phi_i)

    # Should both be positive and finite
    expect_gt(dens_gkw, 0)
    expect_gt(dens_betareg, 0)
    expect_true(is.finite(dens_gkw))
    expect_true(is.finite(dens_betareg))
  }
})

# ==============================================================================
# TEST 6: Model Convergence
# ==============================================================================

test_that("Test 6: Both models should converge successfully", {
  # Verify that both optimizations converge

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2 | x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2 | x1, data = data)

  # gkwreg convergence
  expect_true(fit_gkw$convergence)
  expect_true(is.finite(logLik(fit_gkw)))

  # betareg convergence (check for converged attribute)
  expect_equal(fit_betareg$optim$convergence, 0)
  expect_true(is.finite(logLik(fit_betareg)))
})

# ==============================================================================
# TEST 7: Residuals Comparison
# ==============================================================================

test_that("Test 7: Residuals should be similar between implementations", {
  # Response residuals should be very similar

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2, data = data)

  resid_gkw <- residuals(fit_gkw, type = "response")
  resid_betareg <- residuals(fit_betareg, type = "response")

  # Should be highly correlated
  correlation <- cor(resid_gkw, resid_betareg)
  expect_gt(correlation, 0.99)
})

# ==============================================================================
# TEST 8: Real Data - GasolineYield
# ==============================================================================

test_that("Test 8: Models perform comparably on GasolineYield data", {
  # Test on real dataset

  data("GasolineYield", package = "betareg")

  fit_gkw <- gkwreg(yield ~ batch + temp, data = GasolineYield, family = "beta")
  fit_betareg <- betareg(yield ~ batch + temp, data = GasolineYield)

  # Both should converge
  expect_true(fit_gkw$convergence)

  # Fitted values should be correlated
  fitted_gkw <- fitted(fit_gkw)
  fitted_betareg <- fitted(fit_betareg)

  correlation <- cor(fitted_gkw, fitted_betareg)
  expect_gt(correlation, 0.95) # At least 95% correlation

  # Both should have reasonable RMSE
  rmse_gkw <- sqrt(mean((GasolineYield$yield - fitted_gkw)^2))
  rmse_betareg <- sqrt(mean((GasolineYield$yield - fitted_betareg)^2))

  expect_lt(rmse_gkw, 0.1)
  expect_lt(rmse_betareg, 0.1)
})

# ==============================================================================
# TEST 9: Prediction Consistency
# ==============================================================================

test_that("Test 9: Predictions on new data should be comparable", {
  # Test prediction capabilities

  data <- setup_beta_comparison_data()

  # Split data
  train <- data[1:150, ]
  test <- data[151:200, ]

  fit_gkw <- gkwreg(y ~ x1 + x2, data = train, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2, data = train)

  # Predict on test set
  pred_gkw <- predict(fit_gkw, newdata = test, type = "response")
  pred_betareg <- predict(fit_betareg, newdata = test, type = "response")

  # Predictions should be correlated
  correlation <- cor(pred_gkw, pred_betareg)
  expect_gt(correlation, 0.99)

  # Both should be in (0, 1)
  expect_true(all(pred_gkw > 0 & pred_gkw < 1))
  expect_true(all(pred_betareg > 0 & pred_betareg < 1))
})

# ==============================================================================
# TEST 10: AIC/BIC Comparison
# ==============================================================================

test_that("Test 10: Information criteria should be comparable", {
  # AIC/BIC should be in similar range

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1, data = data)

  aic_gkw <- AIC(fit_gkw)
  aic_betareg <- AIC(fit_betareg)

  bic_gkw <- BIC(fit_gkw)
  bic_betareg <- BIC(fit_betareg)

  # Both should be finite
  expect_true(is.finite(aic_gkw))
  expect_true(is.finite(aic_betareg))
  expect_true(is.finite(bic_gkw))
  expect_true(is.finite(bic_betareg))

  # Lower is better - document if there's a large difference
  aic_diff <- abs(aic_gkw - aic_betareg)
  if (aic_diff > 50) {
    warning(sprintf(
      "Large AIC difference: gkwreg = %.2f, betareg = %.2f",
      aic_gkw, aic_betareg
    ))
  }
})

# ==============================================================================
# TEST 11: Intercept-Only Model Equivalence
# ==============================================================================

test_that("Test 11: Intercept-only models should be very similar", {
  # Simplest case - should have minimal differences

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ 1 | 1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ 1, data = data)

  # Fitted values should all be identical within each model
  fitted_gkw <- fitted(fit_gkw)
  fitted_betareg <- fitted(fit_betareg)

  # Each should be constant
  expect_lt(sd(fitted_gkw), 1e-10)
  expect_lt(sd(fitted_betareg), 1e-10)

  # The constants should be close to observed mean
  mean_y <- mean(data$y)
  expect_equal(as.numeric(fitted_gkw[1]), mean_y, tolerance = 0.1)
  expect_equal(as.numeric(fitted_betareg[1]), mean_y, tolerance = 0.1)
})

# ==============================================================================
# TEST 12: Variable Dispersion Model
# ==============================================================================

test_that("Test 12: Models with variable dispersion should both work", {
  # Test with predictors in both mean and precision/delta

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 | x2, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 | x2, data = data)

  # Both should converge
  expect_true(fit_gkw$convergence)
  expect_equal(fit_betareg$optim$convergence, 0)

  # Fitted values should be correlated
  correlation <- cor(fitted(fit_gkw), fitted(fit_betareg))
  expect_gt(correlation, 0.95)
})

# ==============================================================================
# TEST 13: Parameter Bounds
# ==============================================================================

test_that("Test 13: Estimated parameters should satisfy constraints", {
  # Verify parameter constraints are satisfied

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2 | x1, data = data, family = "beta")

  params <- predict(fit_gkw, type = "parameter")

  # gamma > 0
  expect_true(all(params$gamma > 0))

  # delta >= 0 (shape2 = delta + 1 must be >= 1)
  expect_true(all(params$delta >= 0))

  # Implied shapes should be valid
  shape1 <- params$gamma
  shape2 <- params$delta + 1

  expect_true(all(shape1 > 0))
  expect_true(all(shape2 >= 1))
})

# ==============================================================================
# TEST 15: Coefficient Count
# ==============================================================================

test_that("Test 15: Both models estimate same number of parameters", {
  # For equivalent models, parameter count should match

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2 | x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2 | x1, data = data)

  npar_gkw <- fit_gkw$npar
  npar_betareg <- length(coef(fit_betareg))

  expect_equal(npar_gkw, npar_betareg)
})

# ==============================================================================
# TEST 16: Standard Errors Availability
# ==============================================================================

test_that("Test 16: Standard errors are computed for both models", {
  # Verify inference capability

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1,
    data = data, family = "beta",
    control = gkw_control(hessian = TRUE)
  )
  fit_betareg <- betareg(y ~ x1, data = data)

  # Both should have standard errors
  expect_false(is.null(fit_gkw$se))
  expect_false(is.null(sqrt(diag(vcov(fit_betareg)))))

  # All SEs should be positive
  expect_true(all(fit_gkw$se > 0))
  expect_true(all(sqrt(diag(vcov(fit_betareg))) > 0))
})

# ==============================================================================
# TEST 17: Link Function Consistency
# ==============================================================================

test_that("Test 17: Default link functions are applied correctly", {
  # Verify link functions

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1, data = data, family = "beta")

  # Check stored link functions
  expect_true("gamma" %in% names(fit_gkw$link))
  expect_true("delta" %in% names(fit_gkw$link))

  # Default should be log for both (in gkwreg beta family)
  # Note: betareg uses logit for mu, log for phi
  # This is a fundamental difference
  expect_type(fit_gkw$link$gamma, "character")
  expect_type(fit_gkw$link$delta, "character")
})

# ==============================================================================
# TEST 18: Prediction Types
# ==============================================================================

test_that("Test 18: Different prediction types work correctly", {
  # Test various prediction types for gkwreg

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2, data = data, family = "beta")

  # Response predictions
  pred_response <- predict(fit_gkw, type = "response")
  expect_true(all(pred_response > 0 & pred_response < 1))

  # Parameter predictions
  pred_param <- predict(fit_gkw, type = "parameter")
  expect_true("gamma" %in% names(pred_param))
  expect_true("delta" %in% names(pred_param))
})

# ==============================================================================
# TEST 19: Robustness to Different Seeds
# ==============================================================================

test_that("Test 19: Results are consistent across different random seeds", {
  # Test stability

  results <- lapply(1:5, function(seed) {
    data <- setup_beta_comparison_data(seed = seed * 1000)

    fit_gkw <- gkwreg(y ~ x1, data = data, family = "beta")
    fit_betareg <- betareg(y ~ x1, data = data)

    cor(fitted(fit_gkw), fitted(fit_betareg))
  })

  correlations <- unlist(results)

  # All correlations should be high
  expect_true(all(correlations > 0.95))

  # Should be relatively stable
  expect_lt(sd(correlations), 0.05)
})

# ==============================================================================
# TEST 20: Documentation Example Verification
# ==============================================================================

test_that("Test 20: Package examples produce expected results", {
  # Verify that documented behavior matches reality

  data("GasolineYield", package = "betareg")

  # Fit both models as in documentation
  fit_gkw <- gkwreg(yield ~ batch + temp,
    data = GasolineYield,
    family = "beta"
  )
  fit_betareg <- betareg(yield ~ batch + temp,
    data = GasolineYield
  )

  # Should both work
  expect_s3_class(fit_gkw, "gkwreg")
  expect_s3_class(fit_betareg, "betareg")

  # Should produce summaries
  expect_no_error(summary(fit_gkw))
  expect_no_error(summary(fit_betareg))

  # Should produce predictions
  expect_no_error(predict(fit_gkw))
  expect_no_error(predict(fit_betareg))

  # Fitted values should be reasonable
  expect_true(all(fitted(fit_gkw) > 0 & fitted(fit_gkw) < 1))
  expect_true(all(fitted(fit_betareg) > 0 & fitted(fit_betareg) < 1))
})

# ==============================================================================
# SUMMARY TEST: Overall Compatibility Assessment
# ==============================================================================

test_that("SUMMARY: gkwreg beta family provides compatible alternative to betareg", {
  # Overall assessment

  data <- setup_beta_comparison_data()

  fit_gkw <- gkwreg(y ~ x1 + x2 | x1, data = data, family = "beta")
  fit_betareg <- betareg(y ~ x1 + x2 | x1, data = data)

  # Key compatibility metrics
  fitted_cor <- cor(fitted(fit_gkw), fitted(fit_betareg))
  ll_diff <- abs(as.numeric(logLik(fit_gkw)) - as.numeric(logLik(fit_betareg)))
  ll_ratio <- ll_diff / abs(as.numeric(logLik(fit_betareg)))

  # Document results
  cat("\n")
  cat("===== COMPATIBILITY ASSESSMENT =====\n")
  cat("Fitted values correlation:", fitted_cor, "\n")
  cat("Log-likelihood difference:", ll_diff, "\n")
  cat("Relative LL difference:   ", ll_ratio * 100, "%\n")

  # Basic compatibility requirements
  expect_gt(fitted_cor, 0.95)

  # Both should produce valid statistical models
  expect_true(fit_gkw$convergence,
    info = "gkwreg should converge"
  )
  expect_true(is.finite(logLik(fit_gkw)),
    info = "gkwreg should have finite log-likelihood"
  )
  expect_true(is.finite(logLik(fit_betareg)),
    info = "betareg should have finite log-likelihood"
  )

  cat("=====================================\n\n")
})

Try the gkwreg package in your browser

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

gkwreg documentation built on Nov. 27, 2025, 5:06 p.m.