Nothing
# Tests for precision_study() - Phase 1a
# Input validation and design detection
# Test Data Setup ----
#' Create EP05-style test data (20 days x 2 runs x 2 replicates)
#' @noRd
create_ep05_data <- function(seed = 42, mean_val = 100,
sd_day = 1.5, sd_run = 1.0, sd_error = 2.0) {
set.seed(seed)
n_days <- 20
n_runs <- 2
n_reps <- 2
data <- expand.grid(
day = 1:n_days,
run = 1:n_runs,
replicate = 1:n_reps
)
# Add variance components
day_effect <- rep(rnorm(n_days, 0, sd_day), each = n_runs * n_reps)
run_effect <- rep(rnorm(n_days * n_runs, 0, sd_run), each = n_reps)
error <- rnorm(nrow(data), 0, sd_error)
data$value <- mean_val + day_effect + run_effect + error
data
}
#' Create EP15-style test data (5 days x 5 replicates)
#' @noRd
create_ep15_data <- function(seed = 42, mean_val = 100,
sd_day = 1.5, sd_error = 2.0) {
set.seed(seed)
n_days <- 5
n_reps <- 5
data <- expand.grid(
day = 1:n_days,
replicate = 1:n_reps
)
day_effect <- rep(rnorm(n_days, 0, sd_day), each = n_reps)
error <- rnorm(nrow(data), 0, sd_error)
data$value <- mean_val + day_effect + error
data
}
#' Create multi-site test data (3 sites x 5 days x 5 replicates)
#' @noRd
create_multisite_data <- function(seed = 42, mean_val = 100,
sd_site = 2.0, sd_day = 1.5, sd_error = 2.0) {
set.seed(seed)
n_sites <- 3
n_days <- 5
n_reps <- 5
data <- expand.grid(
site = LETTERS[1:n_sites],
day = 1:n_days,
replicate = 1:n_reps
)
# Generate effects
site_effects <- rnorm(n_sites, 0, sd_site)
day_effects <- rnorm(n_sites * n_days, 0, sd_day)
errors <- rnorm(nrow(data), 0, sd_error)
# Map effects to data rows correctly
# expand.grid varies the first argument fastest, so site varies fastest
site_effect <- site_effects[as.numeric(as.factor(data$site))]
day_idx <- (as.numeric(as.factor(data$site)) - 1) * n_days + as.numeric(as.factor(data$day))
day_effect <- day_effects[day_idx]
data$value <- mean_val + site_effect + day_effect + errors
data
}
#' Create multi-sample test data (multiple concentration levels)
#' @noRd
create_multisample_data <- function(seed = 42) {
set.seed(seed)
# 3 samples at different concentrations
samples <- data.frame(
sample_id = c("Low", "Medium", "High"),
true_mean = c(50, 100, 200)
)
n_days <- 5
n_reps <- 3
data_list <- lapply(1:nrow(samples), function(i) {
sample_data <- expand.grid(
day = 1:n_days,
replicate = 1:n_reps
)
sample_data$sample_id <- samples$sample_id[i]
# CV tends to be higher at low concentrations
cv_factor <- 1 + (1 - samples$true_mean[i] / max(samples$true_mean)) * 0.5
sd_day <- samples$true_mean[i] * 0.015 * cv_factor
sd_error <- samples$true_mean[i] * 0.02 * cv_factor
day_effect <- rep(rnorm(n_days, 0, sd_day), each = n_reps)
error <- rnorm(nrow(sample_data), 0, sd_error)
sample_data$value <- samples$true_mean[i] + day_effect + error
sample_data
})
do.call(rbind, data_list)
}
#' Create day-only precision data (5 days x 5 replicates)
create_day_only_data <- function(grand_mean = 100, sd_day = 2.0, sd_error = 1.5,
seed = 42) {
set.seed(seed)
n_days <- 5
n_reps <- 5
data <- expand.grid(
day = factor(1:n_days),
replicate = factor(1:n_reps)
)
day_effects <- rnorm(n_days, 0, sd_day)
errors <- rnorm(nrow(data), 0, sd_error)
day_effect <- day_effects[as.numeric(data$day)]
data$value <- grand_mean + day_effect + errors
data
}
# Input Validation Tests ----
test_that("precision_study validates data argument", {
# Not a data frame
expect_error(
precision_study(data = "not a data frame", value = "value", day = "day"),
"`data` must be a data frame"
)
# Empty data frame
expect_error(
precision_study(data = data.frame(), value = "value", day = "day"),
"`data` cannot be empty"
)
# NULL data
expect_error(
precision_study(data = NULL, value = "value", day = "day"),
"`data` must be a data frame"
)
})
test_that("precision_study validates value column", {
data <- create_ep15_data()
# Value column doesn't exist
expect_error(
precision_study(data = data, value = "nonexistent", day = "day"),
"Column 'nonexistent' not found in data"
)
# Value column is not numeric
data$char_col <- "text"
expect_error(
precision_study(data = data, value = "char_col", day = "day"),
"Column 'char_col' must be numeric"
)
})
test_that("precision_study validates factor columns exist", {
data <- create_ep15_data()
# Day column doesn't exist
expect_error(
precision_study(data = data, value = "value", day = "nonexistent"),
"Column 'nonexistent' \\(specified for day\\) not found in data"
)
# Site column doesn't exist
expect_error(
precision_study(data = data, value = "value", day = "day",
site = "nonexistent"),
"Column 'nonexistent' \\(specified for site\\) not found in data"
)
# Run column doesn't exist
expect_error(
precision_study(data = data, value = "value", day = "day",
run = "nonexistent"),
"Column 'nonexistent' \\(specified for run\\) not found in data"
)
})
test_that("precision_study requires day factor", {
data <- create_ep15_data()
expect_error(
precision_study(data = data, value = "value", day = NULL),
"At least 'day' factor must be specified"
)
})
test_that("precision_study validates conf_level", {
data <- create_ep15_data()
expect_error(
precision_study(data = data, value = "value", day = "day", conf_level = 0),
"`conf_level` must be a single number between 0 and 1"
)
expect_error(
precision_study(data = data, value = "value", day = "day", conf_level = 1),
"`conf_level` must be a single number between 0 and 1"
)
expect_error(
precision_study(data = data, value = "value", day = "day", conf_level = 1.5),
"`conf_level` must be a single number between 0 and 1"
)
expect_error(
precision_study(data = data, value = "value", day = "day",
conf_level = c(0.9, 0.95)),
"`conf_level` must be a single number between 0 and 1"
)
})
test_that("precision_study validates boot_n", {
data <- create_ep15_data()
expect_error(
precision_study(data = data, value = "value", day = "day", boot_n = 50),
"`boot_n` must be an integer >= 100"
)
expect_error(
precision_study(data = data, value = "value", day = "day", boot_n = 100.5),
"`boot_n` must be an integer >= 100"
)
})
test_that("precision_study handles missing values", {
data <- create_ep15_data()
# Add some NAs
data$value[c(1, 5, 10)] <- NA
# Should produce message about excluded observations
expect_message(
precision_study(data = data, value = "value", day = "day"),
"3 observations excluded due to missing values"
)
})
test_that("precision_study fails with too few observations after NA removal", {
data <- data.frame(
day = 1:5,
value = c(NA, NA, NA, 100, 101)
)
# Only 2 complete observations
expect_error(
suppressMessages(precision_study(data = data, value = "value", day = "day")),
"At least 3 complete observations are required"
)
})
# Design Detection Tests ----
test_that("precision_study detects EP05 design (day/run/replicate)", {
data <- create_ep05_data()
# Suppress the placeholder warning for now
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", run = "run")
)
expect_equal(result$design$type, "single_site")
expect_true(grepl("day", result$design$structure))
expect_true(grepl("run", result$design$structure))
expect_equal(result$design$levels$day, 20)
expect_equal(result$design$levels$run, 2)
expect_true(result$design$balanced)
})
test_that("precision_study detects EP15 design (day/replicate)", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
expect_equal(result$design$type, "single_site")
expect_true(grepl("day", result$design$structure))
expect_false(grepl("/run/", result$design$structure))
expect_equal(result$design$levels$day, 5)
expect_equal(result$design$levels$replicate, 5) # Inferred
expect_true(result$design$balanced)
})
test_that("precision_study detects multi-site design", {
data <- create_multisite_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", site = "site")
)
expect_equal(result$design$type, "multi_site")
expect_true(grepl("site", result$design$structure))
expect_true(grepl("day", result$design$structure))
expect_equal(result$design$levels$site, 3)
expect_equal(result$design$levels$day, 5)
expect_true(result$design$balanced)
})
test_that("precision_study detects unbalanced design", {
# Create unbalanced data
data <- create_ep15_data()
# Remove some observations to make it unbalanced
data <- data[-(1:3), ] # Remove first 3 rows from day 1
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
expect_false(result$design$balanced)
})
test_that("precision_study handles explicit replicate column", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day",
replicate = "replicate")
)
expect_true(grepl("replicate", result$design$structure))
expect_false(grepl("inferred", result$design$structure))
})
test_that("precision_study handles multiple samples", {
data <- create_multisample_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day",
sample = "sample_id")
)
expect_equal(result$design$n_samples, 3)
expect_false(is.null(result$by_sample))
expect_equal(length(result$by_sample), 3)
expect_true(all(c("Low", "Medium", "High") %in% names(result$by_sample)))
expect_equal(length(result$sample_means), 3)
})
# Output Structure Tests ----
test_that("precision_study returns correct class", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
expect_s3_class(result, "precision_study")
expect_s3_class(result, "valytics_precision")
expect_s3_class(result, "valytics_result")
})
test_that("precision_study returns expected structure", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
# Check top-level components
expect_named(result, c("input", "design", "variance_components", "precision",
"anova_table", "by_sample", "sample_means",
"settings", "call"))
# Check input structure
expect_named(result$input, c("data", "n", "n_excluded", "factors", "value_col"))
expect_equal(result$input$n, 25) # 5 days x 5 replicates
expect_equal(result$input$value_col, "value")
# Check design structure
expect_named(result$design, c("type", "structure", "levels", "balanced",
"n_samples", "description"))
# Check settings
expect_named(result$settings, c("conf_level", "ci_method", "boot_n", "method"))
expect_equal(result$settings$conf_level, 0.95)
expect_equal(result$settings$ci_method, "satterthwaite")
expect_equal(result$settings$method, "anova")
})
test_that("precision_study stores factors correctly", {
data <- create_ep05_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", run = "run")
)
expect_equal(result$input$factors$day, "day")
expect_equal(result$input$factors$run, "run")
expect_null(result$input$factors$site)
expect_null(result$input$factors$sample)
})
test_that("precision_study preserves call", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", conf_level = 0.90)
)
expect_true(!is.null(result$call))
expect_true(inherits(result$call, "call"))
})
# Method Argument Tests ----
test_that("precision_study accepts different ci_method values", {
data <- create_ep15_data()
# Satterthwaite (default)
result1 <- suppressWarnings(
precision_study(data = data, value = "value", day = "day",
ci_method = "satterthwaite")
)
expect_equal(result1$settings$ci_method, "satterthwaite")
# MLS
result2 <- suppressWarnings(
precision_study(data = data, value = "value", day = "day",
ci_method = "mls")
)
expect_equal(result2$settings$ci_method, "mls")
# Bootstrap
result3 <- suppressWarnings(
precision_study(data = data, value = "value", day = "day",
ci_method = "bootstrap")
)
expect_equal(result3$settings$ci_method, "bootstrap")
expect_equal(result3$settings$boot_n, 1999)
})
test_that("precision_study accepts method = 'anova'", {
data <- create_ep15_data()
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", method = "anova")
)
expect_equal(result$settings$method, "anova")
})
test_that("precision_study requires lme4 for REML", {
data <- create_ep15_data()
# This test assumes lme4 might not be installed
# If lme4 is installed, the warning will be about "not yet implemented"
# If lme4 is not installed, we get an error about the package
# We'll just check that REML is a valid option
# The actual REML test will be in Phase 1d
result <- suppressWarnings(
tryCatch(
precision_study(data = data, value = "value", day = "day", method = "reml"),
error = function(e) {
if (grepl("lme4", e$message)) {
# Expected if lme4 not installed
NULL
} else {
stop(e)
}
}
)
)
# If we got a result, check it used REML setting
if (!is.null(result)) {
expect_equal(result$settings$method, "reml")
}
})
# Edge Cases ----
test_that("precision_study works with minimum data", {
# Minimum: 3 observations
data <- data.frame(
day = c(1, 1, 2),
value = c(100, 101, 99)
)
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
expect_s3_class(result, "precision_study")
expect_equal(result$input$n, 3)
})
test_that("precision_study handles factor columns that are already factors", {
data <- create_ep15_data()
data$day <- as.factor(data$day)
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day")
)
expect_s3_class(result, "precision_study")
expect_true(is.factor(result$input$data$day))
})
test_that("precision_study handles character factor columns", {
data <- create_multisite_data()
# site is already character (LETTERS)
result <- suppressWarnings(
precision_study(data = data, value = "value", day = "day", site = "site")
)
expect_s3_class(result, "precision_study")
expect_true(is.factor(result$input$data$site))
})
test_that("precision_study generates sensible design description", {
# EP05 style
data1 <- create_ep05_data()
result1 <- suppressWarnings(
precision_study(data = data1, value = "value", day = "day", run = "run")
)
expect_true(grepl("20 days", result1$design$description))
expect_true(grepl("2 runs", result1$design$description))
# Multi-site
data2 <- create_multisite_data()
result2 <- suppressWarnings(
precision_study(data = data2, value = "value", day = "day", site = "site")
)
expect_true(grepl("3 sites", result2$design$description))
expect_true(grepl("5 days", result2$design$description))
})
test_that("CI handles zero variance component gracefully", {
# Create data with very small between-day variance
set.seed(42)
data <- expand.grid(day = factor(1:10), replicate = 1:5)
data$value <- 100 + rnorm(nrow(data), 0, 2) # Only error variance
prec <- precision_study(data, value = "value", day = "day",
ci_method = "satterthwaite")
# Should not error, and CIs should be present
expect_s3_class(prec, "precision_study")
expect_true("ci_lower" %in% names(prec$precision))
})
test_that("CI handles small sample sizes", {
# Minimum viable data
set.seed(42)
data <- expand.grid(day = factor(1:3), replicate = 1:2)
data$value <- 100 + rnorm(nrow(data), 0, 2)
prec <- precision_study(data, value = "value", day = "day",
ci_method = "satterthwaite")
# Should not error
expect_s3_class(prec, "precision_study")
})
# ANOVA Variance Component Tests ----
test_that("ANOVA estimates variance components for day-only design", {
# Create data with known variance components
set.seed(123)
n_days <- 10
n_reps <- 5
# Known variance components
true_var_day <- 4.0 # SD = 2
true_var_error <- 1.0 # SD = 1
data <- expand.grid(day = 1:n_days, replicate = 1:n_reps)
day_effect <- rep(rnorm(n_days, 0, sqrt(true_var_day)), each = n_reps)
error <- rnorm(nrow(data), 0, sqrt(true_var_error))
data$value <- 100 + day_effect + error
result <- precision_study(data, value = "value", day = "day")
# Check structure
expect_true("variance_components" %in% names(result))
expect_true("anova_table" %in% names(result))
vc <- result$variance_components
expect_true("between_day" %in% vc$component)
expect_true("error" %in% vc$component)
expect_true("total" %in% vc$component)
# Check that estimates are reasonable (within ~50% of true values for this sample size)
# Note: with n=50, estimates can vary quite a bit
expect_true(vc$variance[vc$component == "error"] > 0)
expect_true(vc$variance[vc$component == "between_day"] >= 0)
expect_true(vc$variance[vc$component == "total"] > 0)
# Total should equal sum of components
total_var <- vc$variance[vc$component == "total"]
sum_components <- sum(vc$variance[vc$component != "total"])
expect_equal(total_var, sum_components, tolerance = 1e-10)
# Percentages should sum to 100
pct_sum <- sum(vc$pct_total[vc$component != "total"])
expect_equal(pct_sum, 100, tolerance = 1e-10)
})
test_that("ANOVA estimates variance components for day/run design", {
set.seed(456)
n_days <- 10
n_runs <- 2
n_reps <- 2
# Known variance components
true_var_day <- 2.25 # SD = 1.5
true_var_run <- 1.0 # SD = 1.0
true_var_error <- 4.0 # SD = 2.0
data <- expand.grid(day = 1:n_days, run = 1:n_runs, replicate = 1:n_reps)
day_effect <- rep(rnorm(n_days, 0, sqrt(true_var_day)), each = n_runs * n_reps)
run_effect <- rep(rnorm(n_days * n_runs, 0, sqrt(true_var_run)), each = n_reps)
error <- rnorm(nrow(data), 0, sqrt(true_var_error))
data$value <- 100 + day_effect + run_effect + error
result <- precision_study(data, value = "value", day = "day", run = "run")
vc <- result$variance_components
expect_true("between_day" %in% vc$component)
expect_true("between_run" %in% vc$component)
expect_true("error" %in% vc$component)
# All variances should be non-negative
expect_true(all(vc$variance >= 0))
# Check ANOVA table structure
anova <- result$anova_table
expect_true("day" %in% anova$source)
expect_true(any(grepl("run", anova$source)))
expect_true("error" %in% anova$source)
# DF should be correct for balanced design
expect_equal(anova$df[anova$source == "day"], n_days - 1)
expect_equal(anova$df[anova$source == "error"], n_days * n_runs * (n_reps - 1))
})
test_that("ANOVA estimates variance components for multi-site design", {
set.seed(789)
n_sites <- 3
n_days <- 5
n_reps <- 4
true_var_site <- 9.0 # SD = 3
true_var_day <- 4.0 # SD = 2
true_var_error <- 1.0 # SD = 1
data <- expand.grid(
site = LETTERS[1:n_sites],
day = 1:n_days,
replicate = 1:n_reps
)
# Generate random effects
site_effects <- rnorm(n_sites, 0, sqrt(true_var_site))
day_effects <- rnorm(n_sites * n_days, 0, sqrt(true_var_day))
# Map effects correctly - expand.grid varies first argument (site) fastest
site_effect <- site_effects[as.numeric(as.factor(data$site))]
day_idx <- (as.numeric(as.factor(data$site)) - 1) * n_days + as.numeric(as.factor(data$day))
day_effect <- day_effects[day_idx]
error <- rnorm(nrow(data), 0, sqrt(true_var_error))
data$value <- 100 + site_effect + day_effect + error
result <- precision_study(data, value = "value", day = "day", site = "site")
vc <- result$variance_components
expect_true("between_site" %in% vc$component)
expect_true("between_day" %in% vc$component)
expect_true("error" %in% vc$component)
# Site variance should be substantial (we added SD=3)
expect_true(vc$variance[vc$component == "between_site"] > 0)
})
test_that("ANOVA estimates variance components for full site/day/run design", {
set.seed(101)
n_sites <- 2
n_days <- 3
n_runs <- 2
n_reps <- 2
data <- expand.grid(
site = LETTERS[1:n_sites],
day = 1:n_days,
run = 1:n_runs,
replicate = 1:n_reps
)
# Generate random effects
site_effects <- rnorm(n_sites, 0, 2)
day_effects <- rnorm(n_sites * n_days, 0, 1.5)
run_effects <- rnorm(n_sites * n_days * n_runs, 0, 1)
# Map effects correctly - expand.grid varies first argument (site) fastest
site_idx <- as.numeric(as.factor(data$site))
day_idx <- (site_idx - 1) * n_days + as.numeric(as.factor(data$day))
run_idx <- (day_idx - 1) * n_runs + as.numeric(as.factor(data$run))
site_effect <- site_effects[site_idx]
day_effect <- day_effects[day_idx]
run_effect <- run_effects[run_idx]
error <- rnorm(nrow(data), 0, 2)
data$value <- 100 + site_effect + day_effect + run_effect + error
result <- precision_study(
data, value = "value",
day = "day", run = "run", site = "site"
)
vc <- result$variance_components
expect_true("between_site" %in% vc$component)
expect_true("between_day" %in% vc$component)
expect_true("between_run" %in% vc$component)
expect_true("error" %in% vc$component)
expect_true("total" %in% vc$component)
# Should have 5 rows
expect_equal(nrow(vc), 5)
})
test_that("ANOVA handles negative variance estimates correctly", {
# Create data where between-day variance is near zero
# This can lead to negative ANOVA estimates
set.seed(202)
n_days <- 5
n_reps <- 10
# Very small day effect, large error
data <- expand.grid(day = 1:n_days, replicate = 1:n_reps)
day_effect <- rep(rnorm(n_days, 0, 0.1), each = n_reps) # Tiny day effect
error <- rnorm(nrow(data), 0, 5) # Large error
data$value <- 100 + day_effect + error
result <- precision_study(data, value = "value", day = "day")
vc <- result$variance_components
# All variances should be >= 0 (negative estimates set to 0)
expect_true(all(vc$variance >= 0))
# Day variance might be 0 due to negative estimate correction
expect_true(vc$variance[vc$component == "between_day"] >= 0)
})
test_that("ANOVA table has correct structure", {
data <- create_ep05_data()
result <- precision_study(data, value = "value", day = "day", run = "run")
anova <- result$anova_table
# Check columns
expect_true(all(c("source", "df", "ss", "ms") %in% names(anova)))
# Check that SS are non-negative
expect_true(all(anova$ss >= 0, na.rm = TRUE))
# Check that MS = SS / DF (except for total)
for (i in seq_len(nrow(anova))) {
if (!is.na(anova$ms[i]) && anova$df[i] > 0) {
expect_equal(anova$ms[i], anova$ss[i] / anova$df[i], tolerance = 1e-10)
}
}
# Total SS should equal sum of other SS
total_ss <- anova$ss[anova$source == "total"]
other_ss <- sum(anova$ss[anova$source != "total"])
expect_equal(total_ss, other_ss, tolerance = 1e-10)
})
# Precision Summary Tests ----
test_that("Precision summary calculates correct measures for day-only design", {
set.seed(303)
data <- create_ep15_data(mean_val = 100, sd_day = 2, sd_error = 3)
result <- precision_study(data, value = "value", day = "day")
prec <- result$precision
# Should have: Repeatability, Between-day, Within-laboratory precision
expect_true("Repeatability" %in% prec$measure)
expect_true("Between-day" %in% prec$measure)
expect_true("Within-laboratory precision" %in% prec$measure)
# Should NOT have: Between-run, Between-site, Reproducibility
expect_false("Between-run" %in% prec$measure)
expect_false("Between-site" %in% prec$measure)
expect_false("Reproducibility" %in% prec$measure)
# SD values should be positive
expect_true(all(prec$sd > 0))
# CV should be SD / mean * 100
# Grand mean is approximately 100
expect_true(all(prec$cv_pct > 0))
expect_true(all(prec$cv_pct < 20)) # Reasonable range for this data
})
test_that("Precision summary calculates correct measures for day/run design", {
data <- create_ep05_data()
result <- precision_study(data, value = "value", day = "day", run = "run")
prec <- result$precision
# Should have: Repeatability, Between-run, Between-day, Intermediate
expect_true("Repeatability" %in% prec$measure)
expect_true("Between-run" %in% prec$measure)
expect_true("Between-day" %in% prec$measure)
expect_true("Within-laboratory precision" %in% prec$measure)
})
test_that("Precision summary calculates correct measures for multi-site design", {
data <- create_multisite_data()
result <- precision_study(data, value = "value", day = "day", site = "site")
prec <- result$precision
# Should have all measures including site and reproducibility
expect_true("Repeatability" %in% prec$measure)
expect_true("Between-day" %in% prec$measure)
expect_true("Between-site" %in% prec$measure)
expect_true("Reproducibility" %in% prec$measure)
expect_true("Within-laboratory precision" %in% prec$measure)
})
test_that("Within-laboratory precision is correctly calculated", {
set.seed(404)
# Create data with known components
n_days <- 20
n_runs <- 2
n_reps <- 2
var_day <- 4.0
var_run <- 1.0
var_error <- 2.25
data <- expand.grid(day = 1:n_days, run = 1:n_runs, replicate = 1:n_reps)
day_effect <- rep(rnorm(n_days, 0, sqrt(var_day)), each = n_runs * n_reps)
run_effect <- rep(rnorm(n_days * n_runs, 0, sqrt(var_run)), each = n_reps)
error <- rnorm(nrow(data), 0, sqrt(var_error))
data$value <- 100 + day_effect + run_effect + error
result <- precision_study(data, value = "value", day = "day", run = "run")
vc <- result$variance_components
prec <- result$precision
# Within-laboratory precision should be sqrt(var_day + var_run + var_error)
estimated_var_day <- vc$variance[vc$component == "between_day"]
estimated_var_run <- vc$variance[vc$component == "between_run"]
estimated_var_error <- vc$variance[vc$component == "error"]
expected_intermediate_sd <- sqrt(estimated_var_day + estimated_var_run + estimated_var_error)
actual_intermediate_sd <- prec$sd[prec$measure == "Within-laboratory precision"]
expect_equal(actual_intermediate_sd, expected_intermediate_sd, tolerance = 1e-10)
})
test_that("Reproducibility is correctly calculated for multi-site", {
set.seed(505)
data <- create_multisite_data(sd_site = 3, sd_day = 2, sd_error = 1.5)
result <- precision_study(data, value = "value", day = "day", site = "site")
vc <- result$variance_components
prec <- result$precision
# Reproducibility should be sqrt(all variance components)
var_site <- vc$variance[vc$component == "between_site"]
var_day <- vc$variance[vc$component == "between_day"]
var_error <- vc$variance[vc$component == "error"]
expected_repro_sd <- sqrt(var_site + var_day + var_error)
actual_repro_sd <- prec$sd[prec$measure == "Reproducibility"]
expect_equal(actual_repro_sd, expected_repro_sd, tolerance = 1e-10)
})
# Test data generators (for Confidence Intervals) ----
#' Create EP05-style precision data (20 days x 2 runs x 2 replicates)
create_ep05_data <- function(grand_mean = 100, sd_day = 1.5, sd_run = 1.0,
sd_error = 2.0, seed = 42) {
set.seed(seed)
n_days <- 20
n_runs <- 2
n_reps <- 2
data <- expand.grid(
day = factor(1:n_days),
run = factor(1:n_runs),
replicate = factor(1:n_reps)
)
# Add variance components
day_effects <- rnorm(n_days, 0, sd_day)
run_effects <- rnorm(n_days * n_runs, 0, sd_run)
errors <- rnorm(nrow(data), 0, sd_error)
day_effect <- day_effects[as.numeric(data$day)]
run_effect <- run_effects[(as.numeric(data$day) - 1) * n_runs + as.numeric(data$run)]
data$value <- grand_mean + day_effect + run_effect + errors
data
}
#' Create day-only precision data (5 days x 5 replicates)
create_day_only_data <- function(grand_mean = 100, sd_day = 2.0, sd_error = 1.5,
seed = 42) {
set.seed(seed)
n_days <- 5
n_reps <- 5
data <- expand.grid(
day = factor(1:n_days),
replicate = factor(1:n_reps)
)
day_effects <- rnorm(n_days, 0, sd_day)
errors <- rnorm(nrow(data), 0, sd_error)
day_effect <- day_effects[as.numeric(data$day)]
data$value <- grand_mean + day_effect + errors
data
}
#' Create multi-site precision data
create_multisite_data <- function(grand_mean = 100, sd_site = 2.5, sd_day = 1.5,
sd_error = 2.0, seed = 42) {
set.seed(seed)
n_sites <- 3
n_days <- 5
n_reps <- 5
data <- expand.grid(
site = factor(1:n_sites),
day = factor(1:n_days),
replicate = factor(1:n_reps)
)
site_effects <- rnorm(n_sites, 0, sd_site)
day_effects <- rnorm(n_sites * n_days, 0, sd_day)
errors <- rnorm(nrow(data), 0, sd_error)
site_effect <- site_effects[as.numeric(data$site)]
day_idx <- (as.numeric(data$site) - 1) * n_days + as.numeric(data$day)
day_effect <- day_effects[day_idx]
data$value <- grand_mean + site_effect + day_effect + errors
data
}
# Satterthwaite CI tests ----
test_that("Satterthwaite CI produces valid structure for day/run design", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "satterthwaite")
# Check precision data frame has CI columns
expect_true("ci_lower" %in% names(prec$precision))
expect_true("ci_upper" %in% names(prec$precision))
expect_true("cv_ci_lower" %in% names(prec$precision))
expect_true("cv_ci_upper" %in% names(prec$precision))
# Check all measures have CIs
expect_equal(nrow(prec$precision), 4) # repeatability, between-run, between-day, intermediate
# CIs should be numeric
expect_true(is.numeric(prec$precision$ci_lower))
expect_true(is.numeric(prec$precision$ci_upper))
})
test_that("Satterthwaite CI bounds are correctly ordered", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "satterthwaite")
# Lower bound should be less than or equal to upper bound
for (i in seq_len(nrow(prec$precision))) {
if (!is.na(prec$precision$ci_lower[i]) && !is.na(prec$precision$ci_upper[i])) {
expect_true(prec$precision$ci_lower[i] <= prec$precision$ci_upper[i],
info = paste("Row", i, prec$precision$measure[i]))
}
}
# Point estimate should be within CI (for most cases)
for (i in seq_len(nrow(prec$precision))) {
sd_val <- prec$precision$sd[i]
ci_l <- prec$precision$ci_lower[i]
ci_u <- prec$precision$ci_upper[i]
if (!is.na(ci_l) && !is.na(ci_u) && sd_val > 0) {
expect_true(sd_val >= ci_l * 0.99, # Small tolerance for numerical precision
info = paste("Lower bound check for", prec$precision$measure[i]))
expect_true(sd_val <= ci_u * 1.01,
info = paste("Upper bound check for", prec$precision$measure[i]))
}
}
})
test_that("Satterthwaite CI bounds are non-negative", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "satterthwaite")
# All CI bounds for SD should be non-negative
expect_true(all(is.na(prec$precision$ci_lower) | prec$precision$ci_lower >= 0))
expect_true(all(is.na(prec$precision$ci_upper) | prec$precision$ci_upper >= 0))
})
test_that("Satterthwaite CI works with day-only design", {
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day",
ci_method = "satterthwaite")
expect_true("ci_lower" %in% names(prec$precision))
expect_true("ci_upper" %in% names(prec$precision))
# Should have repeatability, between-day, and intermediate
expect_equal(nrow(prec$precision), 3)
})
test_that("Satterthwaite CI respects confidence level", {
data <- create_ep05_data()
prec_95 <- precision_study(data, value = "value", day = "day", run = "run",
conf_level = 0.95, ci_method = "satterthwaite")
prec_90 <- precision_study(data, value = "value", day = "day", run = "run",
conf_level = 0.90, ci_method = "satterthwaite")
prec_99 <- precision_study(data, value = "value", day = "day", run = "run",
conf_level = 0.99, ci_method = "satterthwaite")
# 99% CI should be wider than 95%, which should be wider than 90%
for (i in seq_len(nrow(prec_95$precision))) {
width_90 <- prec_90$precision$ci_upper[i] - prec_90$precision$ci_lower[i]
width_95 <- prec_95$precision$ci_upper[i] - prec_95$precision$ci_lower[i]
width_99 <- prec_99$precision$ci_upper[i] - prec_99$precision$ci_lower[i]
if (!is.na(width_90) && !is.na(width_95) && !is.na(width_99)) {
expect_true(width_99 >= width_95,
info = paste("99% >= 95% for", prec_95$precision$measure[i]))
expect_true(width_95 >= width_90,
info = paste("95% >= 90% for", prec_95$precision$measure[i]))
}
}
})
test_that("Satterthwaite CI width decreases with sample size", {
# Small sample
set.seed(42)
data_small <- expand.grid(day = factor(1:5), replicate = 1:3)
data_small$value <- 100 + rnorm(nrow(data_small), 0, 2)
# Larger sample
set.seed(42)
data_large <- expand.grid(day = factor(1:20), replicate = 1:5)
data_large$value <- 100 + rnorm(nrow(data_large), 0, 2)
prec_small <- precision_study(data_small, value = "value", day = "day",
ci_method = "satterthwaite")
prec_large <- precision_study(data_large, value = "value", day = "day",
ci_method = "satterthwaite")
# Width for repeatability should be smaller with larger sample
width_small <- prec_small$precision$ci_upper[1] - prec_small$precision$ci_lower[1]
width_large <- prec_large$precision$ci_upper[1] - prec_large$precision$ci_lower[1]
expect_true(width_large < width_small)
})
# MLS CI tests ----
test_that("MLS CI produces valid structure", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "mls")
expect_true("ci_lower" %in% names(prec$precision))
expect_true("ci_upper" %in% names(prec$precision))
expect_equal(nrow(prec$precision), 4)
})
test_that("MLS CI bounds are non-negative", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "mls")
expect_true(all(is.na(prec$precision$ci_lower) | prec$precision$ci_lower >= 0))
expect_true(all(is.na(prec$precision$ci_upper) | prec$precision$ci_upper >= 0))
})
test_that("MLS and Satterthwaite CIs have same point estimates", {
data <- create_ep05_data()
prec_sat <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "satterthwaite")
prec_mls <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "mls")
# Point estimates should be identical
expect_equal(prec_sat$precision$sd, prec_mls$precision$sd)
expect_equal(prec_sat$precision$cv_pct, prec_mls$precision$cv_pct)
})
# Bootstrap CI tests ----
test_that("Bootstrap CI produces valid structure", {
skip_on_cran() # Skip on CRAN due to computation time
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day",
ci_method = "bootstrap", boot_n = 199)
expect_true("ci_lower" %in% names(prec$precision))
expect_true("ci_upper" %in% names(prec$precision))
})
test_that("Bootstrap CI bounds contain point estimate", {
skip_on_cran()
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day",
ci_method = "bootstrap", boot_n = 199)
# Point estimate should generally be within CI
for (i in seq_len(nrow(prec$precision))) {
sd_val <- prec$precision$sd[i]
ci_l <- prec$precision$ci_lower[i]
ci_u <- prec$precision$ci_upper[i]
if (!is.na(ci_l) && !is.na(ci_u) && sd_val > 0) {
# Allow some tolerance for bootstrap variability
expect_true(sd_val >= ci_l * 0.8 || ci_l == 0,
info = paste("Lower bound for", prec$precision$measure[i]))
expect_true(sd_val <= ci_u * 1.5,
info = paste("Upper bound for", prec$precision$measure[i]))
}
}
})
test_that("Bootstrap CI bounds are non-negative", {
skip_on_cran()
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day",
ci_method = "bootstrap", boot_n = 199)
expect_true(all(is.na(prec$precision$ci_lower) | prec$precision$ci_lower >= 0))
})
# CV CI tests ----
test_that("CV CIs are correctly calculated from SD CIs", {
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run")
grand_mean <- mean(data$value)
# CV CI should be 100 * SD_CI / mean
for (i in seq_len(nrow(prec$precision))) {
if (!is.na(prec$precision$ci_lower[i])) {
expected_cv_lower <- 100 * prec$precision$ci_lower[i] / grand_mean
expect_equal(prec$precision$cv_ci_lower[i], expected_cv_lower,
tolerance = 0.001)
}
if (!is.na(prec$precision$ci_upper[i])) {
expected_cv_upper <- 100 * prec$precision$ci_upper[i] / grand_mean
expect_equal(prec$precision$cv_ci_upper[i], expected_cv_upper,
tolerance = 0.001)
}
}
})
# Multi-site CI tests ----
test_that("Multi-site design has reproducibility CI", {
data <- create_multisite_data()
prec <- precision_study(data, value = "value", site = "site", day = "day",
ci_method = "satterthwaite")
# Should have reproducibility row
expect_true("Reproducibility" %in% prec$precision$measure)
# Reproducibility should have CI
repro_row <- which(prec$precision$measure == "Reproducibility")
expect_true(!is.na(prec$precision$ci_lower[repro_row]))
expect_true(!is.na(prec$precision$ci_upper[repro_row]))
})
test_that("Multi-site CIs are correctly ordered", {
data <- create_multisite_data()
prec <- precision_study(data, value = "value", site = "site", day = "day",
ci_method = "satterthwaite")
# Reproducibility SD should be >= Intermediate SD
inter_sd <- prec$precision$sd[prec$precision$measure == "Within-laboratory precision"]
repro_sd <- prec$precision$sd[prec$precision$measure == "Reproducibility"]
expect_true(repro_sd >= inter_sd)
})
# Internal helper function tests ----
test_that(".ci_single_variance produces valid CI", {
# This tests the internal function directly
# Access internal function
ci_single <- valytics:::.ci_single_variance
# Test with typical values
ci <- ci_single(variance = 4, df = 20, alpha = 0.05)
expect_named(ci, c("lower", "upper"))
expect_true(ci["lower"] < 4)
expect_true(ci["upper"] > 4)
expect_true(ci["lower"] >= 0)
})
test_that(".ci_variance_sum produces valid CI for sum", {
ci_sum <- valytics:::.ci_variance_sum
# Test with two variance components
ci <- ci_sum(variances = c(4, 2), dfs = c(20, 15), alpha = 0.05)
expect_named(ci, c("lower", "upper"))
expect_true(ci["lower"] < 6) # Sum is 6
expect_true(ci["upper"] > 6)
expect_true(ci["lower"] >= 0)
})
test_that(".ci_single_variance handles edge cases", {
ci_single <- valytics:::.ci_single_variance
# Zero variance
ci <- ci_single(variance = 0, df = 20, alpha = 0.05)
expect_equal(unname(ci["lower"]), 0)
# NA variance
ci <- ci_single(variance = NA, df = 20, alpha = 0.05)
expect_true(is.na(ci["lower"]))
expect_true(is.na(ci["upper"]))
# Zero df
ci <- ci_single(variance = 4, df = 0, alpha = 0.05)
expect_true(is.na(ci["lower"]))
})
# Integration tests ----
test_that("All CI methods produce consistent precision summaries", {
data <- create_day_only_data()
prec_sat <- precision_study(data, value = "value", day = "day",
ci_method = "satterthwaite")
prec_mls <- precision_study(data, value = "value", day = "day",
ci_method = "mls")
# Same structure
expect_equal(nrow(prec_sat$precision), nrow(prec_mls$precision))
expect_equal(prec_sat$precision$measure, prec_mls$precision$measure)
# Same point estimates
expect_equal(prec_sat$precision$sd, prec_mls$precision$sd)
})
test_that("CI method is recorded in settings", {
data <- create_ep05_data()
prec_sat <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "satterthwaite")
prec_mls <- precision_study(data, value = "value", day = "day", run = "run",
ci_method = "mls")
expect_equal(prec_sat$settings$ci_method, "satterthwaite")
expect_equal(prec_mls$settings$ci_method, "mls")
})
# REML Estimation Tests ----
test_that("REML requires lme4 package", {
skip_if_not_installed("lme4")
data <- create_day_only_data()
# Should work when lme4 is available
prec <- precision_study(data, value = "value", day = "day", method = "reml")
expect_s3_class(prec, "precision_study")
})
test_that("REML returns correct structure for day-only design", {
skip_if_not_installed("lme4")
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day", method = "reml")
# Check variance components structure
expect_s3_class(prec$variance_components, "data.frame")
expect_true("between_day" %in% prec$variance_components$component)
expect_true("error" %in% prec$variance_components$component)
expect_true("total" %in% prec$variance_components$component)
# Check all values are numeric and non-negative
expect_true(all(prec$variance_components$variance >= 0))
expect_true(all(prec$variance_components$sd >= 0))
# Method should be recorded
expect_equal(prec$settings$method, "reml")
})
test_that("REML returns correct structure for day/run design", {
skip_if_not_installed("lme4")
data <- create_ep05_data()
prec <- precision_study(data, value = "value", day = "day", run = "run",
method = "reml")
# Check variance components
expect_true("between_day" %in% prec$variance_components$component)
expect_true("between_run" %in% prec$variance_components$component)
expect_true("error" %in% prec$variance_components$component)
# All variances should be non-negative
expect_true(all(prec$variance_components$variance >= 0))
})
test_that("REML produces similar results to ANOVA for balanced data", {
skip_if_not_installed("lme4")
# Create balanced dataset
set.seed(123)
data <- create_ep05_data()
prec_anova <- precision_study(data, value = "value", day = "day", run = "run",
method = "anova")
prec_reml <- precision_study(data, value = "value", day = "day", run = "run",
method = "reml")
# Variance estimates should be similar (not exact due to different methods)
# Allow 20% relative tolerance for comparison
anova_total <- sum(prec_anova$variance_components$variance[
prec_anova$variance_components$component != "total"])
reml_total <- sum(prec_reml$variance_components$variance[
prec_reml$variance_components$component != "total"])
expect_equal(anova_total, reml_total, tolerance = 0.2 * max(anova_total, reml_total))
})
test_that("REML works with multi-site design", {
skip_if_not_installed("lme4")
data <- create_multisite_data()
prec <- precision_study(data, value = "value", site = "site", day = "day",
method = "reml")
# Check site variance component is present
expect_true("between_site" %in% prec$variance_components$component)
expect_true("between_day" %in% prec$variance_components$component)
# All variances non-negative
expect_true(all(prec$variance_components$variance >= 0))
})
test_that("REML CI calculations work", {
skip_if_not_installed("lme4")
data <- create_day_only_data()
prec <- precision_study(data, value = "value", day = "day",
method = "reml", ci_method = "satterthwaite")
# Check CIs are present
expect_true(all(c("ci_lower", "ci_upper") %in% names(prec$precision)))
# CIs should be numeric
expect_true(all(is.numeric(prec$precision$ci_lower)))
expect_true(all(is.numeric(prec$precision$ci_upper)))
})
test_that("REML handles unbalanced data better than ANOVA",
{
skip_if_not_installed("lme4")
# Create unbalanced dataset
set.seed(456)
data <- data.frame(
day = c(rep(1, 4), rep(2, 3), rep(3, 5), rep(4, 2), rep(5, 4)),
value = rnorm(18, mean = 100, sd = 5)
)
data$value <- data$value + as.numeric(factor(data$day)) * 2
# Both methods should run without error
prec_anova <- precision_study(data, value = "value", day = "day", method = "anova")
prec_reml <- precision_study(data, value = "value", day = "day", method = "reml")
expect_s3_class(prec_anova, "precision_study")
expect_s3_class(prec_reml, "precision_study")
# Design should detect unbalanced
expect_false(prec_anova$design$balanced)
expect_false(prec_reml$design$balanced)
})
test_that("REML works with strong between-run variance", {
skip_if_not_installed("lme4")
# Create data with clear run effect
set.seed(42)
n_days <- 5
n_runs <- 2
n_reps <- 3
data_list <- list()
idx <- 1
for (d in 1:n_days) {
day_effect <- rnorm(1, 0, 3)
for (r in 1:n_runs) {
run_effect <- rnorm(1, 0, 5) # Strong run effect
for (rep in 1:n_reps) {
data_list[[idx]] <- data.frame(
day = d, run = r,
value = 100 + day_effect + run_effect + rnorm(1, 0, 2)
)
idx <- idx + 1
}
}
}
data <- do.call(rbind, data_list)
prec <- precision_study(data, value = "value", day = "day", run = "run",
method = "reml")
# Between-run should be non-zero
run_var <- prec$variance_components$variance[
prec$variance_components$component == "between_run"]
expect_true(run_var > 0)
})
test_that("REML with bootstrap CI works", {
skip_if_not_installed("lme4")
skip_on_cran() # Skip due to computation time
data <- create_day_only_data()
# Use fewer bootstrap samples for testing
prec <- precision_study(data, value = "value", day = "day",
method = "reml", ci_method = "bootstrap", boot_n = 199)
expect_s3_class(prec, "precision_study")
expect_true(!any(is.na(prec$precision$ci_lower)))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.