|
| 1 | +#' Add Simultaneous Credible Bands to SIC Posterior Data |
| 2 | +#' |
| 3 | +#' Computes simultaneous credible bands that control the family-wise error rate |
| 4 | +#' across all distances. This ensures the entire curve falls within the band with |
| 5 | +#' (1-alpha)% probability, rather than just pointwise coverage. |
| 6 | +#' |
| 7 | +#' @param sic_data Data frame containing rvar columns (from posterior package) |
| 8 | +#' representing SIC posteriors. Must have a column identifying distance coordinates. |
| 9 | +#' @param distance_col Name of the column containing distance values (default: "distance"). |
| 10 | +#' @param sic_cols Character vector of rvar column names to compute bands for. |
| 11 | +#' If NULL (default), processes all rvar columns except the distance column. |
| 12 | +#' @param alpha Significance level (default 0.05 for 95% simultaneous bands). |
| 13 | +#' @param keep_rvar Logical; if TRUE, keeps the original rvar column. Default FALSE. |
| 14 | +#' |
| 15 | +#' @return A data frame with the original data plus columns for each SIC: |
| 16 | +#' \item{<sic>_mean}{Posterior mean} |
| 17 | +#' \item{<sic>_lower}{Simultaneous lower bound} |
| 18 | +#' \item{<sic>_upper}{Simultaneous upper bound} |
| 19 | +#' |
| 20 | +#' @details |
| 21 | +#' The algorithm: |
| 22 | +#' 1. Extracts mean and SD at each distance for each rvar column |
| 23 | +#' 2. Standardizes posterior deviations: (draw - mean) / SD |
| 24 | +#' 3. Finds maximum absolute deviation across all distances for each draw |
| 25 | +#' 4. Uses the (1-alpha) quantile of these maxima as the critical value |
| 26 | +#' 5. Constructs bands: mean ± critical_value × SD |
| 27 | +#' |
| 28 | +#' Simultaneous bands are wider than pointwise bands but provide stronger guarantees: |
| 29 | +#' the entire curve is contained with (1-alpha)% probability. |
| 30 | +#' |
| 31 | +#' @examples |
| 32 | +#' \dontrun{ |
| 33 | +#' library(dplyr) |
| 34 | +#' library(posterior) |
| 35 | +#' |
| 36 | +#' # Compute SIC posteriors |
| 37 | +#' sics <- compute_sic_posterior(fit, prep, level = "group") |
| 38 | +#' |
| 39 | +#' # Add simultaneous bands |
| 40 | +#' sics_with_bands <- sics %>% |
| 41 | +#' add_simultaneous_bands(alpha = 0.1) |
| 42 | +#' |
| 43 | +#' # Plot |
| 44 | +#' ggplot(sics_with_bands, aes(x = distance, y = sic_mean)) + |
| 45 | +#' geom_ribbon(aes(ymin = sic_lower, ymax = sic_upper), alpha = 0.2) + |
| 46 | +#' geom_line() + |
| 47 | +#' facet_wrap(~source) |
| 48 | +#' } |
| 49 | +#' |
| 50 | +#' @export |
| 51 | +add_simultaneous_bands <- function(sic_data, |
| 52 | + distance_col = "distance", |
| 53 | + sic_cols = NULL, |
| 54 | + alpha = 0.05, |
| 55 | + keep_rvar = FALSE) { |
| 56 | + |
| 57 | + # Validate inputs |
| 58 | + if (!distance_col %in% names(sic_data)) { |
| 59 | + stop(sprintf("distance_col '%s' not found in sic_data", distance_col)) |
| 60 | + } |
| 61 | + |
| 62 | + # Identify rvar columns to process |
| 63 | + if (is.null(sic_cols)) { |
| 64 | + sic_cols <- names(sic_data)[sapply(sic_data, posterior::is_rvar)] |
| 65 | + sic_cols <- setdiff(sic_cols, distance_col) |
| 66 | + } |
| 67 | + |
| 68 | + if (length(sic_cols) == 0) { |
| 69 | + stop("No rvar columns found in sic_data") |
| 70 | + } |
| 71 | + |
| 72 | + # Validate that specified columns exist and are rvars |
| 73 | + missing_cols <- setdiff(sic_cols, names(sic_data)) |
| 74 | + if (length(missing_cols) > 0) { |
| 75 | + stop(sprintf("Columns not found: %s", paste(missing_cols, collapse = ", "))) |
| 76 | + } |
| 77 | + |
| 78 | + non_rvar_cols <- sic_cols[!sapply(sic_data[sic_cols], posterior::is_rvar)] |
| 79 | + if (length(non_rvar_cols) > 0) { |
| 80 | + stop(sprintf("Columns are not rvar type: %s", paste(non_rvar_cols, collapse = ", "))) |
| 81 | + } |
| 82 | + |
| 83 | + # Convert to data frame for processing |
| 84 | + result <- as.data.frame(sic_data) |
| 85 | + |
| 86 | + # Step 1: Extract mean and SD at each distance |
| 87 | + for (col in sic_cols) { |
| 88 | + result[[paste0(col, "_mean")]] <- as.vector(posterior::E(result[[col]])) |
| 89 | + result[[paste0(col, "_sd")]] <- as.vector(posterior::sd(result[[col]])) |
| 90 | + } |
| 91 | + |
| 92 | + # Step 2: Standardize residuals |
| 93 | + # Extract just the rvar columns into a separate data frame |
| 94 | + lp_data <- result[sic_cols] |
| 95 | + |
| 96 | + # Standardize each column |
| 97 | + lp_std <- lapply(sic_cols, function(col) { |
| 98 | + rvar_col <- lp_data[[col]] |
| 99 | + # Compute mean and sd for this column using posterior package functions |
| 100 | + col_mean <- mean(rvar_col) |
| 101 | + col_sd <- posterior::sd(rvar_col) # Use posterior::sd for rvar |
| 102 | + # Standardize |
| 103 | + posterior::as_rvar((rvar_col - col_mean) / col_sd) |
| 104 | + }) |
| 105 | + names(lp_std) <- sic_cols |
| 106 | + |
| 107 | + # Step 3: Find maximum deviation across distances for each posterior draw |
| 108 | + max_dev <- sapply(lp_std, function(col) posterior::rvar_max(abs(col))) |
| 109 | + |
| 110 | + # Step 4: Find critical value for simultaneous coverage |
| 111 | + z_score_band <- sapply(max_dev, quantile, probs = 1 - alpha, na.rm = TRUE) |
| 112 | + |
| 113 | + # Step 5: Construct simultaneous lower and upper bands |
| 114 | + for (col in sic_cols) { |
| 115 | + result[[paste0(col, "_lower")]] <- result[[paste0(col, "_mean")]] - |
| 116 | + z_score_band[[col]] * result[[paste0(col, "_sd")]] |
| 117 | + result[[paste0(col, "_upper")]] <- result[[paste0(col, "_mean")]] + |
| 118 | + z_score_band[[col]] * result[[paste0(col, "_sd")]] |
| 119 | + |
| 120 | + # Remove rvar column if requested |
| 121 | + if (!keep_rvar) { |
| 122 | + result[[col]] <- NULL |
| 123 | + } |
| 124 | + |
| 125 | + # Remove SD column (internal use only) |
| 126 | + result[[paste0(col, "_sd")]] <- NULL |
| 127 | + } |
| 128 | + |
| 129 | + return(result) |
| 130 | +} |
| 131 | + |
| 132 | + |
| 133 | +#' Add Pointwise Credible Bands to SIC Posterior Data |
| 134 | +#' |
| 135 | +#' Computes pointwise credible bands using simple quantiles at each distance. |
| 136 | +#' Unlike simultaneous bands, these only guarantee pointwise coverage - each |
| 137 | +#' individual point has (1-alpha) probability of being in the band, but the |
| 138 | +#' entire curve may not be contained. |
| 139 | +#' |
| 140 | +#' @param sic_data Data frame containing rvar columns (from posterior package) |
| 141 | +#' representing SIC posteriors. Must have a column identifying distance coordinates. |
| 142 | +#' @param distance_col Name of the column containing distance values (default: "distance"). |
| 143 | +#' @param sic_cols Character vector of rvar column names to compute bands for. |
| 144 | +#' If NULL (default), processes all rvar columns except the distance column. |
| 145 | +#' @param alpha Significance level (default 0.05 for 95% pointwise bands). |
| 146 | +#' @param keep_rvar Logical; if TRUE, keeps the original rvar column. Default FALSE. |
| 147 | +#' |
| 148 | +#' @return A data frame with the original data plus columns for each SIC: |
| 149 | +#' \item{<sic>_mean}{Posterior mean} |
| 150 | +#' \item{<sic>_lower}{Pointwise lower bound} |
| 151 | +#' \item{<sic>_upper}{Pointwise upper bound} |
| 152 | +#' |
| 153 | +#' @details |
| 154 | +#' The algorithm: |
| 155 | +#' 1. Extracts mean at each distance for each rvar column |
| 156 | +#' 2. Computes (alpha/2) and (1-alpha/2) quantiles at each distance independently |
| 157 | +#' 3. Returns mean with quantile-based bounds |
| 158 | +#' |
| 159 | +#' Pointwise bands are narrower than simultaneous bands but provide weaker guarantees: |
| 160 | +#' each individual distance has (1-alpha)% coverage, but multiple testing issues arise. |
| 161 | +#' |
| 162 | +#' @examples |
| 163 | +#' \dontrun{ |
| 164 | +#' library(dplyr) |
| 165 | +#' library(posterior) |
| 166 | +#' |
| 167 | +#' # Compute SIC posteriors |
| 168 | +#' sics <- compute_sic_posterior(fit, prep, level = "group") |
| 169 | +#' |
| 170 | +#' # Add pointwise bands |
| 171 | +#' sics_with_bands <- sics %>% |
| 172 | +#' add_pointwise_bands(alpha = 0.05) |
| 173 | +#' |
| 174 | +#' # Compare with simultaneous bands |
| 175 | +#' sics_simul <- add_simultaneous_bands(sics, alpha = 0.05) |
| 176 | +#' } |
| 177 | +#' |
| 178 | +#' @export |
| 179 | +add_pointwise_bands <- function(sic_data, |
| 180 | + distance_col = "distance", |
| 181 | + sic_cols = NULL, |
| 182 | + alpha = 0.05, |
| 183 | + keep_rvar = FALSE) { |
| 184 | + |
| 185 | + # Validate inputs |
| 186 | + if (!distance_col %in% names(sic_data)) { |
| 187 | + stop(sprintf("distance_col '%s' not found in sic_data", distance_col)) |
| 188 | + } |
| 189 | + |
| 190 | + # Identify rvar columns to process |
| 191 | + if (is.null(sic_cols)) { |
| 192 | + sic_cols <- names(sic_data)[sapply(sic_data, posterior::is_rvar)] |
| 193 | + sic_cols <- setdiff(sic_cols, distance_col) |
| 194 | + } |
| 195 | + |
| 196 | + if (length(sic_cols) == 0) { |
| 197 | + stop("No rvar columns found in sic_data") |
| 198 | + } |
| 199 | + |
| 200 | + # Validate that specified columns exist and are rvars |
| 201 | + missing_cols <- setdiff(sic_cols, names(sic_data)) |
| 202 | + if (length(missing_cols) > 0) { |
| 203 | + stop(sprintf("Columns not found: %s", paste(missing_cols, collapse = ", "))) |
| 204 | + } |
| 205 | + |
| 206 | + non_rvar_cols <- sic_cols[!sapply(sic_data[sic_cols], posterior::is_rvar)] |
| 207 | + if (length(non_rvar_cols) > 0) { |
| 208 | + stop(sprintf("Columns are not rvar type: %s", paste(non_rvar_cols, collapse = ", "))) |
| 209 | + } |
| 210 | + |
| 211 | + # Process each SIC column |
| 212 | + result <- sic_data |
| 213 | + |
| 214 | + for (col in sic_cols) { |
| 215 | + rvar_col <- result[[col]] |
| 216 | + |
| 217 | + # Compute mean and quantiles at each distance |
| 218 | + result <- result %>% |
| 219 | + dplyr::mutate( |
| 220 | + !!paste0(col, "_mean") := as.vector(posterior::E(rvar_col)), |
| 221 | + !!paste0(col, "_lower") := sapply(rvar_col, quantile, probs = alpha / 2), |
| 222 | + !!paste0(col, "_upper") := sapply(rvar_col, quantile, probs = 1 - alpha / 2) |
| 223 | + ) |
| 224 | + |
| 225 | + # Remove rvar column if requested |
| 226 | + if (!keep_rvar) { |
| 227 | + result[[col]] <- NULL |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + return(result) |
| 232 | +} |
0 commit comments