diff --git a/NEWS.md b/NEWS.md index 03750c3..39deb65 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,11 +3,12 @@ # cbcTools 0.6.0 - Major revisions made to `cbc_design()` function. -- New `"efficient"` method added for obtaining D-efficient designs. -- Old methods removed: `"full"`, `"orthogonal"`, `"dopt"`, `"CEA"`, and `"Modfed"`. Now there are only `"random"` and `"efficient"` designs -- Bayesian D-efficient designs are now created by setting `method = "efficient"` and a set of priors with a diagonal or full covariance matrix. + - New `"efficient"` method added for obtaining D-efficient designs. + - Old methods removed: `"full"`, `"orthogonal"`, `"dopt"`, `"CEA"`, and `"Modfed"`. Now there are only `"random"` and `"efficient"` designs + - Bayesian D-efficient designs are now created by setting `method = "efficient"` and a set of priors with a diagonal or full covariance matrix. - New `cbc_d_error()` function added to obtain the D-error of a given design. - New `cbc_priors()` function for defining a variety of prior model coefficients. +- Coefficients for levels of an attribute in cbc_priors can be named vectors, addressing #24. - New `cbc_levels()` function for viewing a summary of the levels in a design. # cbcTools 0.5.2 diff --git a/R/methods.R b/R/methods.R index e09425c..94a33d4 100644 --- a/R/methods.R +++ b/R/methods.R @@ -1,3 +1,64 @@ +# Print method for cbc_priors objects +#' @export +print.cbc_priors <- function(x, ...) { + cat("CBC Prior Specification:\n\n") + + # Print means with full level information + cat("Means:\n") + for (attr in names(x$means)) { + cat(" ", attr, ":\n", sep = "") + + if (x$attr_info[[attr]]$type == "continuous") { + # Get all unique values in sequential order + levels <- sort(unique(x$attr_info[[attr]]$levels)) + cat(" Continuous attribute with levels:\n", + " ", paste(levels, collapse = ", "), "\n", + " Coefficient: ", round(x$means[[attr]], 3), "\n", + sep = "") + } else { + # Find reference level (the one not in names of means) + all_levels <- x$attr_info[[attr]]$levels + coef_levels <- names(x$means[[attr]]) + ref_level <- setdiff(all_levels, coef_levels) + + cat(" Categorical attribute (reference level: ", ref_level, ")\n", sep = "") + for (level in names(x$means[[attr]])) { + cat(" ", level, ": ", round(x$means[[attr]][[level]], 3), "\n", sep = "") + } + } + cat("\n") + } + + # Print SDs if present + if (!is.null(x$sd)) { + cat("Standard Deviations:\n") + for (attr in names(x$sd)) { + cat(" ", attr, ": ", sep = "") + if (length(x$sd[[attr]]) == 1) { + cat(round(x$sd[[attr]], 3), "\n") + } else { + cat(paste(round(x$sd[[attr]], 3), collapse = ", "), "\n") + } + } + cat("\n") + } + + # Print correlation if present + if (!is.null(x$correlation)) { + cat("Correlation Matrix:\n") + print(round(x$correlation, 3)) + cat("\n") + } + + # Print distributions if present + if (!is.null(x$distribution)) { + cat("Distributions:\n") + print(x$distribution) + } + + invisible(x) +} + #' Methods for cbc_models objects #' #' Miscellaneous methods for `cbc_models` class objects. diff --git a/R/priors.R b/R/priors.R index 6f59f6f..0b1f616 100644 --- a/R/priors.R +++ b/R/priors.R @@ -1,3 +1,210 @@ +#' Create prior specifications for CBC models +#' +#' Creates a standardized prior specification object for use in CBC analysis +#' functions like cbc_choices() and cbc_d_error(). Supports both named and unnamed +#' vectors for categorical attributes, where named vectors explicitly map levels +#' to coefficients. +#' +#' @param profiles A data frame of profiles created by cbc_profiles() +#' @param ... Named arguments for each parameter's priors. For continuous variables, +#' provide a single value. For categorical variables, provide either: +#' - An unnamed vector of values one less than the number of levels (dummy coding) +#' - A named vector mapping specific levels to coefficients (remaining level becomes reference) +#' @param sd Optional named list of standard deviations for random parameters +#' @param correlation Optional correlation matrix for random parameters +#' @param distribution Optional named vector specifying distribution type for random +#' parameters ("normal" or "lognormal") +#' @return A structured prior specification object +#' @export +#' @examples +# # Create profiles for an example conjoint about apples +# profiles <- cbc_profiles( +# price = c(1, 1.5, 2, 2.5, 3), +# type = c("Fuji", "Gala", "Honeycrisp"), +# freshness = c("Poor", "Average", "Excellent") +# ) +# +# # Example 1: Simple fixed parameters with unnamed vectors +# priors <- cbc_priors( +# profiles = profiles, +# price = -0.5, +# type = c(0.2, 0.3), # Dummy-coded categorical +# freshness = c(0.4, 0.8) # Dummy-coded categorical +# ) + +#' # Example 2: Using named vectors for categorical variables +#' priors <- cbc_priors( +#' profiles = profiles, +#' price = -0.5, +#' type = c("Fuji" = 0.2, "Gala" = 0.3), # Honeycrisp as reference +#' freshness = c("Poor" = -0.4, "Average" = 0.1) # Excellent as reference +#' ) +#' +#' # Example 3: Mixed approach with random parameters +#' priors <- cbc_priors( +#' profiles = profiles, +#' price = -0.5, +#' type = c("Fuji" = 0.2, "Gala" = 0.3), +#' freshness = c(0.4, 0.8), +#' sd = list( +#' price = 0.4, +#' type = c(0.4, 0.4) +#' ) +#' ) +cbc_priors <- function(profiles, ..., sd = NULL, correlation = NULL, distribution = NULL) { + # Validate profiles input + if (!inherits(profiles, "data.frame") || !"profileID" %in% names(profiles)) { + stop("'profiles' must be a data frame created by cbc_profiles()") + } + + # Get attribute information from profiles + attr_info <- get_attribute_info(profiles) + + # Capture the means + means <- list(...) + + # Validate attribute names + check_attribute_names(means, attr_info) + + # Process and validate each mean parameter + means <- process_mean_parameters(means, attr_info) + + # Validate sd if provided + if (!is.null(sd)) { + check_sd_specification(sd, means, attr_info) + } + + # Validate distribution if provided + if (!is.null(distribution)) { + check_distribution_specification(distribution, attr_info) + } + + # Create the prior specification object + prior_spec <- list( + means = means, + sd = sd, + correlation = correlation, + distribution = distribution, + attr_info = attr_info # Store attribute info for printing + ) + + # Add class for potential method dispatch + class(prior_spec) <- c("cbc_priors", "list") + + return(prior_spec) +} + +# Helper function to extract attribute information from profiles for cbc_priors +# Helper function to extract attribute information from profiles +get_attribute_info <- function(profiles) { + # Remove profileID column + attrs <- profiles[, -which(names(profiles) == "profileID")] + + # Get information for each attribute + attr_info <- lapply(names(attrs), function(attr) { + values <- attrs[[attr]] + is_continuous <- is.numeric(values) + + if (is_continuous) { + list( + type = "continuous", + range = range(values), + levels = unique(values) # Store all unique values for continuous attributes + ) + } else { + list( + type = "categorical", + levels = if (is.factor(values)) levels(values) else unique(values) + ) + } + }) + names(attr_info) <- names(attrs) + return(attr_info) +} + +# Helper function to validate attribute names +check_attribute_names <- function(means, attr_info) { + # Check for missing attributes + missing_attrs <- setdiff(names(attr_info), names(means)) + if (length(missing_attrs) > 0) { + stop("Missing prior specifications for attributes: ", + paste(missing_attrs, collapse = ", ")) + } + + # Check for extra attributes + extra_attrs <- setdiff(names(means), names(attr_info)) + if (length(extra_attrs) > 0) { + stop("Prior specifications provided for non-existent attributes: ", + paste(extra_attrs, collapse = ", ")) + } +} + +# Helper function to process and validate mean parameters +process_mean_parameters <- function(means, attr_info) { + result <- lapply(names(means), function(attr) { + value <- means[[attr]] + info <- attr_info[[attr]] + + if (info$type == "continuous") { + if (!is.numeric(value) || length(value) != 1) { + stop("Prior for continuous attribute '", attr, "' must be a single numeric value") + } + return(value) + } else { + # For categorical attributes + if (is.null(names(value))) { + # Unnamed vector - validate length + if (length(value) != length(info$levels) - 1) { + stop("Prior for categorical attribute '", attr, "' must have ", + length(info$levels) - 1, " values (one less than number of levels)") + } + # Add names based on non-reference levels + names(value) <- info$levels[-1] + } else { + # Named vector - validate names + invalid_levels <- setdiff(names(value), info$levels) + if (length(invalid_levels) > 0) { + stop("Invalid levels specified for attribute '", attr, "': ", + paste(invalid_levels, collapse = ", ")) + } + } + return(value) + } + }) + + # Add the attribute names to the result list + names(result) <- names(means) + return(result) +} + +# Helper function to validate sd specification +check_sd_specification <- function(sd, means, attr_info) { + if (!is.list(sd)) { + stop("sd must be a named list") + } + + # Check that all sd parameters correspond to existing means + invalid_sds <- setdiff(names(sd), names(means)) + if (length(invalid_sds) > 0) { + stop("SD specified for non-existent parameters: ", + paste(invalid_sds, collapse = ", ")) + } + + # Check lengths match for each parameter + for (param in names(sd)) { + if (attr_info[[param]]$type == "continuous") { + if (length(sd[[param]]) != 1) { + stop("SD for continuous attribute '", param, "' must be a single value") + } + } else { + if (length(sd[[param]]) != length(means[[param]])) { + stop("SD for categorical attribute '", param, + "' must match length of means specification") + } + } + } +} + #' Display attribute levels and dummy coding for a CBC design #' #' Shows how categorical variables will be dummy coded and what each coefficient @@ -29,18 +236,12 @@ #' cbc_levels(design) cbc_levels <- function(design, exclude = NULL) { # Get attribute columns (excluding metadata) - meta_cols <- c("profileID", "respID", "qID", "altID", "obsID", "blockID") - attr_cols <- setdiff(names(design), meta_cols) + attr_cols <- get_var_names(design) if (!is.null(exclude)) { attr_cols <- setdiff(attr_cols, exclude) } - # Function to determine if variable is continuous - is_continuous <- function(x) { - is.numeric(x) && length(unique(x)) > 10 - } - # Process each attribute attr_info <- list() @@ -49,7 +250,7 @@ cbc_levels <- function(design, exclude = NULL) { for (attr in attr_cols) { values <- design[[attr]] - if (is_continuous(values)) { + if (is.numeric(values)) { # Continuous variable cat(sprintf("%-12s: Continuous variable\n", attr)) cat(sprintf(" Range: %.2f to %.2f\n", @@ -106,178 +307,3 @@ cbc_levels <- function(design, exclude = NULL) { invisible(attr_info) } - -#' Create prior specifications for CBC models -#' -#' Creates a standardized prior specification object for use in CBC analysis -#' functions like cbc_choices() and cbc_d_error(). -#' -#' @param ... Named arguments for each parameter's priors. For continuous variables, -#' provide a single value. For categorical variables, provide a vector of values -#' one less than the number of levels (dummy coding). -#' @param sd Optional named list of standard deviations for random parameters -#' @param correlation Optional correlation matrix for random parameters -#' @param distribution Optional named vector specifying distribution type for random -#' parameters ("normal" or "lognormal") -#' @return A structured prior specification object -#' @export -#' @examples -#' # Example 1: Simple fixed parameters -#' priors <- cbc_priors( -#' price = -0.5, -#' type = c(0.2, 0.3), # Dummy-coded categorical -#' freshness = c(0.4, 0.8) # Dummy-coded categorical -#' ) -#' -#' # Example 2: Independent random parameters -#' priors <- cbc_priors( -#' price = -0.5, -#' type = c(0.2, 0.3), -#' freshness = c(0.4, 0.8), -#' sd = list( -#' price = 0.4, -#' type = c(0.4, 0.4) -#' ) -#' ) -#' -#' # Example 3: Correlated random parameters -#' priors <- cbc_priors( -#' price = -0.5, -#' type = c(0.2, 0.3), -#' freshness = c(0.4, 0.8), -#' sd = list( -#' price = 0.4, -#' type = c(0.4, 0.4), -#' freshness = c(0.4, 0.4) -#' ), -#' correlation = matrix(c( -#' 1.00, 0.30, 0.30, 0.00, 0.00, -#' 0.30, 1.00, 0.30, 0.00, 0.00, -#' 0.30, 0.30, 1.00, 0.00, 0.00, -#' 0.00, 0.00, 0.00, 1.00, 0.30, -#' 0.00, 0.00, 0.00, 0.30, 1.00 -#' ), 5, 5) -#' ) -#' -#' # Example 4: Mixed distributions -#' priors <- cbc_priors( -#' price = -0.5, -#' type = c(0.2, 0.3), -#' freshness = c(0.4, 0.8), -#' sd = list( -#' price = 0.4, -#' type = c(0.4, 0.4) -#' ), -#' distribution = c( -#' price = "lognormal", -#' type = "normal" -#' ) -#' ) -cbc_priors <- function(..., sd = NULL, correlation = NULL, distribution = NULL) { - # Capture the means - means <- list(...) - - # Validate inputs - if (!is.null(sd) && !is.list(sd)) { - stop("sd must be a named list") - } - if (!is.null(distribution) && !is.vector(distribution)) { - stop("distribution must be a named vector") - } - - # Create the prior specification object - prior_spec <- list( - means = means, - sd = sd, - correlation = correlation, - distribution = distribution - ) - - # Add class for potential method dispatch - class(prior_spec) <- c("cbc_priors", "list") - - return(prior_spec) -} - -# Helper function to process priors into matrix form for internal use -process_priors <- function(prior_spec) { - if (!inherits(prior_spec, "cbc_priors")) { - stop("Prior specification must be created with cbc_priors()") - } - - # Extract means and create flattened vector - means <- unlist(prior_spec$means) - param_names <- names(means) - - # Process standard deviations - if (!is.null(prior_spec$sd)) { - sds <- unlist(prior_spec$sd) - # Fill in zeros for fixed parameters - all_sds <- rep(0, length(means)) - names(all_sds) <- names(means) - all_sds[names(sds)] <- sds - } else { - all_sds <- rep(0, length(means)) - names(all_sds) <- names(means) - } - - # Create covariance matrix - if (!is.null(prior_spec$correlation)) { - if (nrow(prior_spec$correlation) != length(means)) { - stop("Correlation matrix dimensions don't match number of parameters") - } - sigma <- diag(all_sds) %*% prior_spec$correlation %*% diag(all_sds) - } else { - sigma <- diag(all_sds^2) - } - - # Process distributions - if (!is.null(prior_spec$distribution)) { - distributions <- prior_spec$distribution - # Validate distribution types - valid_dist <- c("normal", "lognormal") - if (!all(distributions %in% valid_dist)) { - stop("Invalid distribution type. Must be 'normal' or 'lognormal'") - } - } else { - distributions <- rep("normal", length(means)) - names(distributions) <- names(means) - } - - return(list( - means = means, - sigma = sigma, - param_names = param_names, - distributions = distributions - )) -} - -# Print method for cbc_priors objects -#' @export -print.cbc_priors <- function(x, ...) { - cat("CBC Prior Specification:\n\n") - - # Print means - cat("Means:\n") - print(x$means) - - # Print SDs if present - if (!is.null(x$sd)) { - cat("\nStandard Deviations:\n") - print(x$sd) - } - - # Print correlation if present - if (!is.null(x$correlation)) { - cat("\nCorrelation Matrix:\n") - print(x$correlation) - } - - # Print distributions if present - if (!is.null(x$distribution)) { - cat("\nDistributions:\n") - print(x$distribution) - } - - invisible(x) -}