diff --git a/NAMESPACE b/NAMESPACE index e372811..7d8f2c0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,17 +1,19 @@ # Generated by roxygen2: do not edit by hand S3method(as.character,pool_strat) +S3method(design_effect,fixed_design) +S3method(design_effect,variable_design) S3method(format,pool_strat) S3method(print,pool_strat) S3method(print,power_size_results) export(design_effect) -export(design_effect_random) export(detection_errors) export(detection_errors_cluster) export(fi_pool) export(fi_pool_cluster) export(fi_pool_cluster_random) export(fi_pool_random) +export(fixed_design) export(nb_catch) export(optimise_random_prevalence) export(optimise_sN_prevalence) @@ -24,3 +26,4 @@ export(power_pool_random) export(power_size_results) export(sample_size_pool) export(sample_size_pool_random) +export(variable_design) diff --git a/R/check_input.R b/R/check_input.R index 3e68ebf..da72ac3 100644 --- a/R/check_input.R +++ b/R/check_input.R @@ -1,3 +1,5 @@ +# NOTE: Will discontinue - use check_input2.R + check_input <- function(argument_name, input_value) { # Wrapper function to triage arguments, so one function can be used for all # inputs instead of remembering which one to use. diff --git a/R/check_input2.R b/R/check_input2.R new file mode 100644 index 0000000..65eba67 --- /dev/null +++ b/R/check_input2.R @@ -0,0 +1,20 @@ +check_geq2 <- function(val, min) { + name <- deparse(substitute(val)) # get name of variable + if (!is.numeric(val)) { + stop(glue::glue("{name} must be numeric, not {class(val)}.")) + } + if (val < min) { + stop(glue::glue("{name} must be >= {min}.")) + } +} + +check_in_range2 <- function(val) { + name <- deparse(substitute(val)) # get name of variable + if(!is.numeric(val)) { + stop(glue::glue("{name} must be numeric, not {class(val)}.")) + } + if (val < 0 | val > 1) { + message(glue::glue("{name} must be a numeric value between 0 and 1, inclusive.")) + stop(glue::glue("{name} = {val}")) + } +} \ No newline at end of file diff --git a/R/design_effect.R b/R/design_effect.R index a02aec3..5b55648 100644 --- a/R/design_effect.R +++ b/R/design_effect.R @@ -9,22 +9,11 @@ #' to be multiplied by a factor of D to achieve the same degree of precision in #' estimating prevalence as a simple random survey with individual tests. The #' functions support cluster and simple random sampling with perfect or -#' imperfect tests, and either fixed sample sizes (`design_effect()`) or random -#' sample sizes (`design_effect_random()`). +#' imperfect tests, and either fixed sample sizes +#' (`design_effect(fixed_design, ...)`) or variable sample sizes +#' (`design_effect(variable_design, ...)`). #' -#' @param pool_size numeric The number of units per pool. Must be a numeric -#' value greater than or equal to 0. -#' @param pool_number numeric The number of pools per cluster. Must be a numeric -#' value greater than or equal to 0. -#' @param catch_dist An object of class `distribution` (e.g. produced by -#' `nb_catch()`) defining the distribution of the possible catch. If -#' `correlation = 0` the catch is for the whole survey. For `correlation > 0` -#' the catch is per cluster (i.e. cluster size). -#' @param pool_strat function Defines a rule for how a number of units will be -#' divided into pools. Must take a single numeric argument and return a named -#' list of pool sizes and pool numbers. `pool_max_size()` and -#' `pool_target_number` provide convenience functions for defining common -#' pooling strategies. +#' @param x a sample_design object #' @param prevalence numeric The proportion of units that carry the marker of #' interest (i.e. true positive). Must be be a numeric value between 0 and 1, #' inclusive of both. @@ -35,63 +24,51 @@ #' (there are no differences units within a single cluster). A value of 0 #' indicates that units within clusters are no more correlated than units in #' different clusters. -#' @param sensitivity numeric The probability that the test correctly identifies -#' a true positive. Must be a numeric value between 0 and 1, inclusive of -#' both. A value of 1 indicates that the test can perfectly identify all true -#' positives. -#' @param specificity numeric The probability that the test correctly identifies -#' a true negative. Must be a numeric value between 0 and 1, inclusive of -#' both. A value of 1 indicates that the test can perfectly identify all true -#' negatives. #' @param form string The distribution used to model the cluster-level #' prevalence and correlation of units within cluster. Select one of "beta", -#' "logitnorm" or "cloglognorm". See details. +#' "logitnorm" or "cloglognorm". #' #' @return A numeric value of the design effect `D`. #' @export -#' +#' #' @examples -#' design_effect( -#' pool_size = 5, pool_number = 10, prevalence = 0.01, -#' correlation = 0.05, sensitivity = 0.99, specificity = 0.95 -#' ) -design_effect <- function(pool_size, - pool_number, - prevalence, - correlation, - sensitivity, - specificity, - form = "beta"){ - - check_input("pool_size", pool_size) - check_input("pool_number", pool_number) - check_input("prevalence", prevalence) - check_input("correlation", correlation) - check_input("sensitivity", sensitivity) - check_input("specificity", specificity) - check_input("form", form) +#' design_effect(fixed_design(10, 2), prevalence = 0.01, correlation = 0.05) +#' +#' vd <- variable_design(nb_catch(10, 13), pool_target_number(20)) +#' design_effect(vd, prevalence = 0.01, correlation = 0.05) +design_effect <- function(x, prevalence, correlation, form) { + check_in_range2(prevalence) + check_in_range2(correlation) + # No input check for form as done in downstream functions/methods + UseMethod("design_effect") +} - pool_number * pool_size * fi_pool(pool_size = 1, prevalence, sensitivity, specificity) * +#' @rdname design_effect +#' @method design_effect fixed_design +#' @export +design_effect.fixed_design <- function(x, + prevalence, + correlation, + form = "beta") { + + x$pool_number * x$pool_size * fi_pool(pool_size = 1, prevalence, x$sensitivity, x$specificity) * solve(fi_pool_cluster( - pool_size, pool_number, prevalence, - correlation, sensitivity, specificity, form) + x$pool_size, x$pool_number, prevalence, + correlation, x$sensitivity, x$specificity, form) )[1, 1] } - #' @rdname design_effect +#' @method design_effect variable_design #' @export -design_effect_random <- function(catch_dist, - pool_strat, - prevalence, - correlation, - sensitivity, - specificity, - form = "beta") { +design_effect.variable_design <- function(x, + prevalence, + correlation, + form = "beta") { - mean(catch_dist) * fi_pool(pool_size = 1, prevalence, sensitivity, specificity) * + mean(x$catch_dist) * fi_pool(pool_size = 1, prevalence, x$sensitivity, x$specificity) * solve(fi_pool_cluster_random( - catch_dist, pool_strat, prevalence, - correlation, sensitivity, specificity, form) + x$catch_dist, x$pool_strat, prevalence, + correlation, x$sensitivity, x$specificity, form) )[1, 1] } diff --git a/R/sample_design.R b/R/sample_design.R new file mode 100644 index 0000000..dc65c41 --- /dev/null +++ b/R/sample_design.R @@ -0,0 +1,103 @@ +#' S3 sample_design constructors +#' +#' Stores parameters related to the sampling design. Aims to reduce having to +#' input each param separately across functions (e.g. power/optimise). Can +#' either be of class `fixed_design` or `variable_design`. +#' +#' @param pool_size numeric/NULL The number of units per pool. Must be a numeric +#' value greater than 0. `fixed_design` only. +#' @param pool_number numeric/NULL The number of pools per cluster. Numeric +#' inputs must be an integer greater than or equal to 1. `fixed_design` only. +#' @param catch_dist An object of class `distribution` (e.g. produced by +#' `nb_catch()`) defining the distribution of the possible catch. If +#' `correlation = 0` the catch is for the whole survey. For `correlation > 0` +#' the catch is per cluster (i.e. cluster size). `variable_design` only. +#' @param pool_strat function Defines a rule for how a number of units will be +#' divided into pools. Must take a single numeric argument and return a named +#' list of pool sizes and pool numbers. `pool_max_size()` and +#' `pool_target_number` provide convenience functions for defining common +#' pooling strategies. `variable_design` only. +#' @param sensitivity numeric The probability that the test correctly identifies +#' a true positive. Must be a numeric value between 0 and 1, inclusive of +#' both. A value of 1 indicates that the test can perfectly identify all true +#' positives. +#' @param specificity numeric The probability that the test correctly identifies +#' a true negative. Must be a numeric value between 0 and 1, inclusive of +#' both. A value of 1 indicates that the test can perfectly identify all true +#' negatives. +#' +#' @return An object of class \code{sample_design} +#' @export +#' +#' @examples +#' fd_perfect <- fixed_design(pool_size = 10) +#' +#' fd_imperfect <- fixed_design( +#' pool_size = 10, pool_number = NULL, sensitivity = 0.95, specificity = 0.99 +#' ) +#' +#' vd_target <- variable_design( +#' catch_dist = nb_catch(10, 11), +#' pool_strat = pool_target_number(20) +#' ) +#' +#' vd_max <- variable_design( +#' catch_dist = nb_catch(10, 11), +#' pool_strat = pool_max_size(20) +#' ) +#' +#' vd_max_imperfect <- variable_design( +#' catch_dist = nb_catch(10, 11), +#' pool_strat = pool_max_size(20), +#' sensitivity = 0.95, +#' specificity = 0.98 +#' ) +fixed_design <- function(pool_size = NULL, + pool_number = NULL, + sensitivity = 1, + specificity = 1) { + + # allow NULLs for optimise functions to identify which + # variable should be optimised + if (!is.null(pool_size)) { + check_geq2(pool_size, 0) + } + if (!is.null(pool_number)) { + check_geq2(pool_number, 0) + } + # sens and spec cannot be NULL + check_in_range2(sensitivity) + check_in_range2(specificity) + + structure( + list( + pool_size = pool_size, + pool_number = pool_number, + sensitivity = sensitivity, + specificity = specificity + ), + class = c("fixed_design", "sample_design") + ) +} + +#' @rdname fixed_design +#' @export +variable_design <- function(catch_dist, + pool_strat, + sensitivity = 1, + specificity = 1) { + + # sens and spec cannot be NULL + check_in_range2(sensitivity) + check_in_range2(specificity) + + structure( + list( + catch_dist = catch_dist, + pool_strat = pool_strat, + sensitivity = sensitivity, + specificity = specificity + ), + class = c("variable_design", "sample_design") + ) +} diff --git a/man/design_effect.Rd b/man/design_effect.Rd index b9e2aa4..fd6de9d 100644 --- a/man/design_effect.Rd +++ b/man/design_effect.Rd @@ -2,35 +2,18 @@ % Please edit documentation in R/design_effect.R \name{design_effect} \alias{design_effect} -\alias{design_effect_random} +\alias{design_effect.fixed_design} +\alias{design_effect.variable_design} \title{Calculate the design effect for pooled testing.} \usage{ -design_effect( - pool_size, - pool_number, - prevalence, - correlation, - sensitivity, - specificity, - form = "beta" -) +design_effect(x, prevalence, correlation, form) -design_effect_random( - catch_dist, - pool_strat, - prevalence, - correlation, - sensitivity, - specificity, - form = "beta" -) +\method{design_effect}{fixed_design}(x, prevalence, correlation, form = "beta") + +\method{design_effect}{variable_design}(x, prevalence, correlation, form = "beta") } \arguments{ -\item{pool_size}{numeric The number of units per pool. Must be a numeric -value greater than or equal to 0.} - -\item{pool_number}{numeric The number of pools per cluster. Must be a numeric -value greater than or equal to 0.} +\item{x}{a sample_design object} \item{prevalence}{numeric The proportion of units that carry the marker of interest (i.e. true positive). Must be be a numeric value between 0 and 1, @@ -44,30 +27,9 @@ A value of 1 indicates that units within clusters are perfectly correlated indicates that units within clusters are no more correlated than units in different clusters.} -\item{sensitivity}{numeric The probability that the test correctly identifies -a true positive. Must be a numeric value between 0 and 1, inclusive of -both. A value of 1 indicates that the test can perfectly identify all true -positives.} - -\item{specificity}{numeric The probability that the test correctly identifies -a true negative. Must be a numeric value between 0 and 1, inclusive of -both. A value of 1 indicates that the test can perfectly identify all true -negatives.} - \item{form}{string The distribution used to model the cluster-level prevalence and correlation of units within cluster. Select one of "beta", -"logitnorm" or "cloglognorm". See details.} - -\item{catch_dist}{An object of class `distribution` (e.g. produced by -`nb_catch()`) defining the distribution of the possible catch. If -`correlation = 0` the catch is for the whole survey. For `correlation > 0` -the catch is per cluster (i.e. cluster size).} - -\item{pool_strat}{function Defines a rule for how a number of units will be -divided into pools. Must take a single numeric argument and return a named -list of pool sizes and pool numbers. `pool_max_size()` and -`pool_target_number` provide convenience functions for defining common -pooling strategies.} +"logitnorm" or "cloglognorm".} } \value{ A numeric value of the design effect `D`. @@ -82,12 +44,13 @@ effect `D>1` (`D<1`) indicates that the pooling/sampling strategy reduces to be multiplied by a factor of D to achieve the same degree of precision in estimating prevalence as a simple random survey with individual tests. The functions support cluster and simple random sampling with perfect or -imperfect tests, and either fixed sample sizes (`design_effect()`) or random -sample sizes (`design_effect_random()`). +imperfect tests, and either fixed sample sizes +(`design_effect(fixed_design, ...)`) or variable sample sizes +(`design_effect(variable_design, ...)`). } \examples{ -design_effect( - pool_size = 5, pool_number = 10, prevalence = 0.01, - correlation = 0.05, sensitivity = 0.99, specificity = 0.95 - ) +design_effect(fixed_design(10, 2), prevalence = 0.01, correlation = 0.05) + +vd <- variable_design(nb_catch(10, 13), pool_target_number(20)) +design_effect(vd, prevalence = 0.01, correlation = 0.05) } diff --git a/man/fixed_design.Rd b/man/fixed_design.Rd new file mode 100644 index 0000000..db24e45 --- /dev/null +++ b/man/fixed_design.Rd @@ -0,0 +1,76 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sample_design.R +\name{fixed_design} +\alias{fixed_design} +\alias{variable_design} +\title{S3 sample_design constructors} +\usage{ +fixed_design( + pool_size = NULL, + pool_number = NULL, + sensitivity = 1, + specificity = 1 +) + +variable_design(catch_dist, pool_strat, sensitivity = 1, specificity = 1) +} +\arguments{ +\item{pool_size}{numeric/NULL The number of units per pool. Must be a numeric +value greater than 0. `fixed_design` only.} + +\item{pool_number}{numeric/NULL The number of pools per cluster. Numeric +inputs must be an integer greater than or equal to 1. `fixed_design` only.} + +\item{sensitivity}{numeric The probability that the test correctly identifies +a true positive. Must be a numeric value between 0 and 1, inclusive of +both. A value of 1 indicates that the test can perfectly identify all true +positives.} + +\item{specificity}{numeric The probability that the test correctly identifies +a true negative. Must be a numeric value between 0 and 1, inclusive of +both. A value of 1 indicates that the test can perfectly identify all true +negatives.} + +\item{catch_dist}{An object of class `distribution` (e.g. produced by +`nb_catch()`) defining the distribution of the possible catch. If +`correlation = 0` the catch is for the whole survey. For `correlation > 0` +the catch is per cluster (i.e. cluster size). `variable_design` only.} + +\item{pool_strat}{function Defines a rule for how a number of units will be +divided into pools. Must take a single numeric argument and return a named +list of pool sizes and pool numbers. `pool_max_size()` and +`pool_target_number` provide convenience functions for defining common +pooling strategies. `variable_design` only.} +} +\value{ +An object of class \code{sample_design} +} +\description{ +Stores parameters related to the sampling design. Aims to reduce having to +input each param separately across functions (e.g. power/optimise). Can +either be of class `fixed_design` or `variable_design`. +} +\examples{ +fd_perfect <- fixed_design(pool_size = 10) + +fd_imperfect <- fixed_design( + pool_size = 10, pool_number = NULL, sensitivity = 0.95, specificity = 0.99 +) + +vd_target <- variable_design( + catch_dist = nb_catch(10, 11), + pool_strat = pool_target_number(20) +) + +vd_max <- variable_design( + catch_dist = nb_catch(10, 11), + pool_strat = pool_max_size(20) +) + +vd_max_imperfect <- variable_design( + catch_dist = nb_catch(10, 11), + pool_strat = pool_max_size(20), + sensitivity = 0.95, + specificity = 0.98 +) +} diff --git a/tests/testthat/test-check_input2.R b/tests/testthat/test-check_input2.R new file mode 100644 index 0000000..9f09b51 --- /dev/null +++ b/tests/testthat/test-check_input2.R @@ -0,0 +1,18 @@ +test_that("check_geq2", { + x <- 1 + expect_silent(check_geq2(x, 1)) + expect_error(check_geq2(x, 2), "x must be >= 2.") + y <- "a" + expect_error(check_geq2(y, 1), "y must be numeric, not character.") +}) + +test_that("check_in_range2", { + x <- 0.01 + expect_silent(check_in_range2(x)) + x <- -1 + expect_error(check_in_range2(x), "x = -1") + x <- 2 + expect_error(check_in_range2(x), "x = 2") + y <- "a" + expect_error(check_in_range2(y), "y must be numeric, not character.") +}) \ No newline at end of file diff --git a/tests/testthat/test-design_effect.R b/tests/testthat/test-design_effect.R index a7d89cf..adff430 100644 --- a/tests/testthat/test-design_effect.R +++ b/tests/testthat/test-design_effect.R @@ -1,52 +1,30 @@ -### design_effect() ----------------------------------------------------------- -test_that("design_effect() gives consistent output for basic tests", { +# fixed_design ---- +fd <- fixed_design( + pool_size = 5, pool_number = 10, sensitivity = 0.99, specificity = 0.95 +) + +test_that("fixed design_effect() gives consistent output for basic tests", { # This one has reasonable inputs expect_equal( - design_effect( - pool_size = 5, - pool_number = 10, - prevalence = 0.01, - correlation = 0.05, - sensitivity = 0.99, - specificity = 0.95), + design_effect(fd, prevalence = 0.01, correlation = 0.05), 0.7240988, tolerance = 1e-7 ) expect_equal( - design_effect( - pool_size = 10, - pool_number = 10, - prevalence = 0.9, - sensitivity = 1, - specificity = 1, - correlation = 0.9, - form = "beta" - ), - 118.9243, - tolerance = 1e-4 + design_effect(fixed_design(10, 10), prevalence = 0.9, correlation = 0.9), + 118.9243, tolerance = 1e-4 ) expect_equal( design_effect( - pool_size = 10, - pool_number = 10, - prevalence = 0.2, - sensitivity = 0.9, - specificity = 0.8, - correlation = 0.2, - form = "beta" - ), - 26.50055, - tolerance = 1e-5 + fixed_design(10, 10, 0.9, 0.8), prevalence = 0.2, correlation = 0.2), + 26.50055, tolerance = 1e-5 ) }) test_that("design_effect() fails for some very unusual parameters because integral in call to fi_pool_cluster() appears starts to have numeric issues", { expect_error( design_effect( - pool_size = 100, - pool_number = 10, + fixed_design(100, 10, 1, 0.7), prevalence = 0.8, - sensitivity = 1, - specificity = 0.7, correlation = 0.8, form = "cloglognorm" ), @@ -54,13 +32,18 @@ test_that("design_effect() fails for some very unusual parameters because integr ) }) +### variable_design() ---- +vd_target <- variable_design(nb_catch(5, 7), pool_target_number(10)) +vd_max <- variable_design(nb_catch(5, 7), pool_max_size(10)) -test_that("bad inputs caught in design_effect()", { - expect_error(design_effect(pool_size = TRUE), "TRUE is a logical") - expect_error(design_effect(pool_size = 5, pool_number = 10, prevalence = 10), "10 is > 1") - expect_error(design_effect(pool_size = 5, pool_number = 10, prevalence = 0.01, correlation = 0.1, sensitivity = 1, specificity = 1, form = "binomal"), - "form must be one of 'beta', 'logitnorm', 'cloglognorm', or 'discrete'.") +test_that("variable design_effect()", { + expect_equal( + design_effect(vd_target, prevalence = 0.01, correlation = 0.05), + 1.256262, tolerance = 1e-6 + ) + expect_equal( + design_effect(vd_max, prevalence = 0.01, correlation = 0.05), + 3.726256, tolerance = 1e-6 + ) }) -### design_effect_random() ---------------------------------------------------- - diff --git a/tests/testthat/test-sample_design.R b/tests/testthat/test-sample_design.R new file mode 100644 index 0000000..a547daf --- /dev/null +++ b/tests/testthat/test-sample_design.R @@ -0,0 +1,59 @@ +# fixed_design ---- +## fixtures ---- +fixed_perfect <- fixed_design( + pool_size = 10, pool_number = NULL, sensitivity = 1, specificity = 1 +) + +fixed_null <- fixed_design() # sens/spec == 1, pool_size/num == NULL + +## test ---- +test_that("fixed_design constructor", { + expect_equal(class(fixed_perfect), c("fixed_design", "sample_design")) + expect_equal(fixed_perfect$pool_size, 10) + expect_equal(fixed_perfect$pool_number, NULL) + expect_equal(fixed_perfect$sensitivity, 1) + expect_equal(fixed_perfect$specificity, 1) +}) + +test_that("fixed_design default", { + expect_equal(class(fixed_perfect), c("fixed_design", "sample_design")) + expect_equal(fixed_null$pool_size, NULL) + expect_equal(fixed_null$pool_number, NULL) + expect_equal(fixed_null$sensitivity, 1) + expect_equal(fixed_null$specificity, 1) +}) + +test_that("fixed_design bad inputs caught", { + expect_error(fixed_design(pool_size = -1)) + expect_error(fixed_design(pool_number = -1)) +}) + +# variable_design ---- +## fixtures ---- +var_target <- variable_design( + catch_dist = nb_catch(5, 10), + pool_strat = pool_target_number(20) +) + +var_max <- variable_design( + catch_dist = nb_catch(5, 10), + pool_strat = pool_max_size(20) +) + +## tests ---- +test_that("variable_design constructor (target_size)", { + expect_equal(class(var_target), c("variable_design", "sample_design")) + expect_equal(var_target$catch_dist, nb_catch(5, 10)) + expect_equal(var_target$pool_strat, pool_target_number(20)) + expect_equal(var_target$sensitivity, 1) + expect_equal(var_target$specificity, 1) +}) + +test_that("variable_design constructor (max_size)", { + expect_equal(class(var_max), c("variable_design", "sample_design")) + expect_equal(var_max$pool_strat, pool_max_size(20)) +}) + +test_that("null variable_design", { + expect_error(variable_design()) +})