diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 7be9b5d..a010cb1 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -8,6 +8,8 @@ on: name: R-CMD-check +permissions: read-all + jobs: R-CMD-check: runs-on: ${{ matrix.config.os }} @@ -18,18 +20,16 @@ jobs: fail-fast: false matrix: config: - - {os: macOS-latest, r: 'release'} + - {os: macos-latest, r: 'release'} - {os: windows-latest, r: 'release'} - #- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - - {os: ubuntu-latest, r: 'release'} - #- {os: ubuntu-latest, r: 'oldrel-1'} + - {os: ubuntu-latest, r: 'release'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-pandoc@v2 @@ -41,6 +41,10 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: rcmdcheck + cache-version: 2 + extra-packages: any::rcmdcheck + needs: check - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true diff --git a/NAMESPACE b/NAMESPACE index d2f491d..75f5e01 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -68,6 +68,7 @@ export(log_TOST) export(np_ses) export(perm_t_test) export(plot_cor) +export(plot_htest_est) export(plot_pes) export(plot_smd) export(powerTOSTone) diff --git a/NEWS.md b/NEWS.md index 196a1b7..45284d6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,18 @@ NEWS - Added `perm_t_test` function to allow for permutation tests for equivalence using TOST - Update `brunner_munzel` function to allow TOST directly - Update functions to disallow `paired = TRUE` when formula method utilized. +- Improved `plot.TOSTt` for `type = "simple"`: + - Raw estimate plot now appears on top (was on bottom) + - Decision text and equivalence bounds now displayed at top of plot + - Added `layout` parameter: "stacked" (default) or "combined" for a single faceted plot +- Improved `plot.TOSTt` for `type = "tnull"`: + - Now shows only one-sided rejection regions appropriate to the test type + - Equivalence tests: lower bound shows right tail, upper bound shows left tail + - Minimal effect tests: lower bound shows left tail, upper bound shows right tail +- Added `plot_htest_est()` function to create simple estimate plots from any `htest` object + - Displays point estimate with confidence interval + - Handles null values (single or equivalence bounds) as reference lines + - Automatically handles two-sample t-test estimates by computing mean difference # TOSTER v0.8.7 - Update documentation to make it clear what the "eqb" argument does within the `wilcox_TOST` function. diff --git a/R/htest_helpers.R b/R/htest_helpers.R index 4ddb076..4c8ecf1 100644 --- a/R/htest_helpers.R +++ b/R/htest_helpers.R @@ -413,3 +413,264 @@ printable_pval = function(pval, return(pval) } + +#' @title Plot Estimate from 'htest' Object +#' +#' @description +#' `r lifecycle::badge('stable')` +#' +#' Creates a simple point estimate plot with confidence interval from any 'htest' object +#' that contains an estimate and confidence interval. This provides a visual representation +#' of the effect size and its uncertainty, similar to a forest plot. +#' +#' @param htest An S3 object of class 'htest' containing at minimum an `estimate` and +#' `conf.int` component. Examples include output from `t.test()`, `cor.test()`, +#' or TOSTER functions converted with `as_htest()`. +#' @param alpha Significance level for determining the confidence level label. +#' @param describe Logical. If TRUE (default), includes a concise statistical description +#' in the plot subtitle showing the test statistic, p-value, estimate, confidence interval, +#' and the null hypothesis. +#' +#' @details +#' The function creates a horizontal point-range plot showing: +#' \itemize{ +#' \item Point estimate (black dot) +#' \item Confidence interval (horizontal line) +#' \item Null value(s) as dashed vertical reference line(s) +#' } +#' +#' For two-sample t-tests, R's `t.test()` returns both group means as the estimate +#' rather than their difference. This function automatically computes the difference to display +#' a single meaningful estimate with its confidence interval. +#' +#' If the 'htest' object contains equivalence bounds (two values in `null.value`), +#' both bounds are displayed as dashed vertical lines. +#' +#' When `describe = TRUE`, the plot includes a three-line subtitle: +#' \enumerate{ +#' \item Test statistic and p-value +#' \item Point estimate and confidence interval +#' \item Null hypothesis statement +#' } +#' The method name appears as the plot title. +#' +#' @return A `ggplot` object that can be further customized using ggplot2 functions. +#' +#' @examples +#' # Standard t-test +#' t_result <- t.test(extra ~ group, data = sleep) +#' plot_htest_est(t_result) +#' +#' # One-sample t-test +#' t_one <- t.test(sleep$extra, mu = 0) +#' plot_htest_est(t_one) +#' +#' # Correlation test +#' cor_result <- cor.test(mtcars$mpg, mtcars$wt) +#' plot_htest_est(cor_result) +#' +#' # TOST result converted to htest +#' tost_res <- t_TOST(extra ~ group, data = sleep, eqb = 1) +#' plot_htest_est(as_htest(tost_res)) +#' +#' # Without description +#' plot_htest_est(t_result, describe = FALSE) +#' +#' @import ggplot2 +#' @import ggdist +#' @family htest +#' @export +plot_htest_est <- function(htest, alpha = NULL, describe = TRUE) { + + if (!inherits(htest, "htest")) { + stop("Input must be an object of class 'htest'") + } + + if (is.null(htest$estimate)) { + stop("Cannot create estimate plot: htest object has no estimate") + } + + if (is.null(htest$conf.int)) { + stop("Cannot create estimate plot: htest object has no confidence interval") + } + + # Handle two-sample t-test case where estimate contains both group means + estimate <- htest$estimate + estimate_name <- names(estimate) + + if (grepl("two sample t-test", htest$method, ignore.case = TRUE) && + length(estimate) > 1) { + estimate <- estimate[1] - estimate[2] + estimate_name <- "mean difference" + } else if (length(estimate) > 1) { + # For other cases with multiple estimates, warn and use first + warning("htest object has multiple estimates; using first estimate only") + estimate <- estimate[1] + estimate_name <- names(htest$estimate)[1] + } + + # Get confidence interval + ci_lower <- min(htest$conf.int) + ci_upper <- max(htest$conf.int) + conf_level <- attr(htest$conf.int, "conf.level") + + if (is.null(conf_level)) { + if (!is.null(alpha)) { + conf_level <- 1 - alpha + } else { + conf_level <- 0.95 + message("No confidence level found in htest object. Defaulting to 95%.") + } + } + + # Determine label for facet + if (is.null(estimate_name) || length(estimate_name) == 0) { + facet_label <- "Estimate" + } else { + # Capitalize first letter + facet_label <- paste0(toupper(substr(estimate_name, 1, 1)), + substr(estimate_name, 2, nchar(estimate_name))) + } + + # Create data frame for plotting (include facet_label in the data) + df_plot <- data.frame( + estimate = unname(estimate), + lower.ci = ci_lower, + upper.ci = ci_upper, + facet_label = facet_label, + stringsAsFactors = FALSE + ) + + # Build description for subtitle if requested + if (describe) { + # Build concise description similar to describe_htest but shorter + desc_parts <- c() + + # Add test statistic if available + if (!is.null(htest$statistic)) { + stat_name <- names(htest$statistic) + stat_val <- rounder_stat(unname(htest$statistic), digits = 3) + + if (!is.null(htest$parameter)) { + par_val <- rounder_stat(unname(htest$parameter), digits = 2) + stat_str <- paste0(stat_name, "(", par_val, ") = ", stat_val) + } else { + stat_str <- paste0(stat_name, " = ", stat_val) + } + desc_parts <- c(desc_parts, stat_str) + } + + # Add p-value if available + if (!is.null(htest$p.value)) { + desc_parts <- c(desc_parts, printable_pval(htest$p.value, digits = 3)) + } + + # Build first line: test statistic and p-value + line1 <- paste(desc_parts, collapse = ", ") + + # Build second line: estimate and CI + est_str <- paste0(estimate_name, " = ", + rounder_stat(unname(estimate), digits = 3)) + ci_str <- paste0(round(conf_level * 100), "% CI [", + rounder_stat(ci_lower, digits = 3), ", ", + rounder_stat(ci_upper, digits = 3), "]") + line2 <- paste(est_str, ci_str, sep = ", ") + + # Build third line: null hypothesis + line3 <- NULL + if (!is.null(htest$null.value) && !is.null(htest$alternative)) { + null_name <- names(htest$null.value) + if (is.null(null_name) || length(null_name) == 0) { + null_name <- estimate_name + } + + if (length(htest$null.value) == 1) { + # Standard hypothesis test - show null based on alternative + null_rel <- switch(htest$alternative, + two.sided = "is equal to", + less = "is greater than or equal to", + greater = "is less than or equal to", + "is equal to") + line3 <- paste0("null: ", null_name, " ", null_rel, " ", + rounder_stat(unname(htest$null.value), digits = 3)) + } else if (length(htest$null.value) == 2) { + # Equivalence or minimal effect test + null_vals <- sort(unname(htest$null.value)) + if (htest$alternative == "equivalence") { + line3 <- paste0("null: ", null_name, " < ", rounder_stat(null_vals[1], digits = 3), + " or > ", rounder_stat(null_vals[2], digits = 3)) + } else if (htest$alternative == "minimal.effect") { + line3 <- paste0("null: ", rounder_stat(null_vals[1], digits = 3), + " < ", null_name, " < ", rounder_stat(null_vals[2], digits = 3)) + } else { + # Fallback for other cases with two bounds + line3 <- paste0("null: ", null_name, " in [", + rounder_stat(null_vals[1], digits = 3), ", ", + rounder_stat(null_vals[2], digits = 3), "]") + } + } + } else if (!is.null(htest$null.value)) { + # No alternative specified, just show null value + null_name <- names(htest$null.value) + if (is.null(null_name) || length(null_name) == 0) { + null_name <- estimate_name + } + if (length(htest$null.value) == 1) { + line3 <- paste0("null: ", null_name, " = ", + rounder_stat(unname(htest$null.value), digits = 3)) + } + } + + # Combine lines + if (!is.null(line3)) { + subtitle_text <- paste(line1, line2, line3, sep = "\n") + } else { + subtitle_text <- paste(line1, line2, sep = "\n") + } + title_text <- htest$method + } else { + subtitle_text <- NULL + title_text <- htest$method + } + + # Build the plot + p <- ggplot(df_plot, + aes(x = estimate, + y = 1, + xmin = lower.ci, + xmax = upper.ci)) + + geom_pointrange() + + facet_grid(~facet_label) + + theme_tidybayes() + + labs(caption = paste0(conf_level * 100, "% Confidence Interval"), + title = title_text, + subtitle = subtitle_text) + + theme(strip.text = element_text(face = "bold", size = 10), + plot.title = element_text(size = 11), + plot.subtitle = element_text(size = 9), + axis.title.x = element_blank(), + axis.title.y = element_blank(), + axis.text.y = element_blank(), + axis.ticks.y = element_blank()) + + # Add null value reference line(s) + if (!is.null(htest$null.value)) { + null_vals <- unname(htest$null.value) + + if (length(null_vals) == 1) { + # Single null value (standard hypothesis test) + p <- p + geom_vline(xintercept = null_vals, linetype = "dashed") + } else if (length(null_vals) == 2) { + # Two null values (equivalence bounds) + p <- p + + geom_vline(xintercept = null_vals[1], linetype = "dashed") + + geom_vline(xintercept = null_vals[2], linetype = "dashed") + + scale_x_continuous(sec.axis = dup_axis( + breaks = round(null_vals, 3), + name = "" + )) + } + } + + return(p) +} diff --git a/R/methods.TOSTt.R b/R/methods.TOSTt.R index 5ca56ea..0f63a79 100644 --- a/R/methods.TOSTt.R +++ b/R/methods.TOSTt.R @@ -4,10 +4,11 @@ #' #' @param x object of class `TOSTt`. #' @param digits Number of digits to print for p-values -#' @param type Type of plot to produce. Default is a consonance density plot "cd". Consonance plots (type = "cd") and null distribution plots (type = "tnull") can also be produced. Note: null distribution plots only available for estimates = "raw". +#' @param type Type of plot to produce. Default is "simple" which shows point estimates with confidence intervals. Other options include consonance plots ("c"), consonance density plots ("cd"), and null distribution plots ("tnull"). Note: null distribution plots only available for estimates = "raw". #' @param ci_lines Confidence interval lines for plots. Default is 1-alpha*2 (e.g., alpha = 0.05 is 90%) #' @param ci_shades Confidence interval shades when plot type is "cd". #' @param estimates indicator of what estimates to plot; options include "raw" or "SMD". Default is is both: c("raw","SMD"). +#' @param layout Layout for displaying multiple estimates. Options are "stacked" (default, separate plots stacked vertically) or "combined" (single faceted plot with shared legend). Only applies when both "raw" and "SMD" are in estimates. #' @param ... further arguments passed through, see description of return value for details.. #' #' @return @@ -119,8 +120,10 @@ plot.TOSTt <- function(x, estimates = c("raw","SMD"), ci_lines, ci_shades, + layout = c("stacked", "combined"), ...){ type = match.arg(type) + layout = match.arg(layout) low_eqd = x$eqb$low_eq[2] high_eqd = x$eqb$high_eq[2] @@ -177,12 +180,79 @@ plot.TOSTt <- function(x, ci_print = x$effsize$conf.level[1] + # Build subtitle with decision text and equivalence bounds + eqb_text = paste0("Equivalence bounds: [", + round(low_eqt, 3), ", ", + round(high_eqt, 3), "] (raw)") + subtitle_text = paste0(x$decision$TOST, "\n", + x$decision$ttest, "\n", + eqb_text) + # Get estimates for mean ---- df_t = x$effsize[1,] # Get estimates for SMD ---- df_d = x$effsize[2,] + # Check if we should use combined layout + both_estimates = "SMD" %in% estimates && "raw" %in% estimates + + if(both_estimates && layout == "combined"){ + # Combined faceted plot ---- + # Create combined data frame with scale indicator + df_combined = rbind( + data.frame( + estimate = df_t$estimate, + lower.ci = df_t$lower.ci, + upper.ci = df_t$upper.ci, + type = paste0(x_label, " (raw)"), + low_eq = low_eqt, + high_eq = high_eqt, + stringsAsFactors = FALSE + ), + data.frame( + estimate = df_d$estimate, + lower.ci = df_d$lower.ci, + upper.ci = df_d$upper.ci, + type = paste0(x$smd$smd_label, " (standardized)"), + low_eq = low_eqd, + high_eq = high_eqd, + stringsAsFactors = FALSE + ) + ) + # Preserve order: raw first, then SMD + df_combined$type = factor(df_combined$type, + levels = c(paste0(x_label, " (raw)"), + paste0(x$smd$smd_label, " (standardized)"))) + + plts <- ggplot(df_combined, + aes(x = estimate, + y = 1, + xmin = lower.ci, + xmax = upper.ci)) + + geom_pointrange() + + geom_vline(aes(xintercept = low_eq), linetype = "dashed") + + geom_vline(aes(xintercept = high_eq), linetype = "dashed") + + facet_wrap(~type, scales = "free_x") + + theme_tidybayes() + + labs(title = subtitle_text, + caption = paste0(ci_print*100, "% Confidence Interval")) + + theme(strip.text = element_text(face = "bold", size = 10), + plot.title = element_text(size = 10), + axis.title.x = element_blank(), + axis.title.y = element_blank(), + axis.text.y = element_blank(), + axis.ticks.y = element_blank()) + + return(plts) + } + + # Stacked layout (default) ---- + # Build facet labels with scale indicator + raw_facet_label = paste0(x_label, " (raw)") + smd_facet_label = paste0(x$smd$smd_label, " (standardized)") + + # Raw plot (now shown on top with subtitle) t_plot <- ggplot(df_t, aes(x=estimate, @@ -195,18 +265,19 @@ plot.TOSTt <- function(x, scale_x_continuous(sec.axis = dup_axis(breaks=c(round(low_eqt,round_t), round(high_eqt,round_t)), name = "")) + - facet_grid(~as.character(x_label)) + + facet_grid(~as.character(raw_facet_label)) + theme_tidybayes() + labs(caption = paste0(ci_print*100,"% Confidence Interval"), - subtitle = paste0(x$decision$TOST, " \n", x$decision$ttest)) + + title = subtitle_text) + theme(strip.text = element_text(face = "bold", size = 10), - plot.subtitle = element_text(size = 10), + plot.title = element_text(size = 10), axis.title.x = element_blank(), axis.title.y=element_blank(), axis.text.y=element_blank(), axis.ticks.y=element_blank()) + # SMD plot (now shown on bottom) d_plot <- ggplot(df_d, aes(x=estimate, @@ -219,7 +290,7 @@ plot.TOSTt <- function(x, scale_x_continuous(sec.axis = dup_axis(breaks=c(round(low_eqd,round_t), round(high_eqd,round_t)), name = "")) + - facet_grid(~as.character(x$smd$smd_label)) + + facet_grid(~as.character(smd_facet_label)) + labs(caption = paste0(ci_print*100,"% Confidence Interval")) + theme_tidybayes() + theme(strip.text = element_text(face = "bold", @@ -229,14 +300,10 @@ plot.TOSTt <- function(x, axis.text.y=element_blank(), axis.ticks.y=element_blank()) - - - # add the legend to the row we made earlier. Give it one-third of - # the width of one plot (via rel_widths). - - if("SMD" %in% estimates && "raw" %in% estimates){ - plts = plot_grid(d_plot, - t_plot, + # Stack plots: raw on top, SMD on bottom + if(both_estimates){ + plts = plot_grid(t_plot, + d_plot, ncol = 1) } @@ -595,23 +662,14 @@ plot.TOSTt <- function(x, warning("Multiple CI lines provided; only first element will be used.") } - if("Equilvalence" %in% x$hypothesis){ - METhyp = TRUE - } else { - METhyp = FALSE - } - points = data.frame( - type = x_label, - mu = c(x$effsize$estimate[1]), - param = c(round(unname(x$TOST$df[1]), 0)), - sigma = c(x$TOST$SE[1]), - lambda = c(0), - est = c(x$effsize$estimate[1]), - low = c(x$eqb$low_eq[1]), - high = c(x$eqb$high_eq[1]), - alpha = c(x$alpha), - stringsAsFactors = FALSE - ) + # Determine if this is equivalence or MET hypothesis + is_equivalence = grepl("Equivalence", x$hypothesis, ignore.case = TRUE) + + # Common parameters + se = x$TOST$SE[1] + df_t = round(unname(x$TOST$df[1]), 0) + + # Data for point estimate and CI points = data.frame( x_label = x_label, point = x$effsize$estimate[1], @@ -619,53 +677,88 @@ plot.TOSTt <- function(x, ci_low = x$effsize$upper.ci[1], stringsAsFactors = FALSE ) + points_l = data.frame( - mu = c(x$eqb$low_eq[1]), - param = c(round(unname(x$TOST$df[1]), 0)), - sigma = c(x$TOST$SE[1]), + mu = c(low_eqt), + param = c(df_t), + sigma = c(se), lambda = c(0), stringsAsFactors = FALSE ) points_u = data.frame( - mu = c(x$eqb$high_eq[1]), - param = c(round(unname(x$TOST$df[1]), 0)), - sigma = c(x$TOST$SE[1]), + mu = c(high_eqt), + param = c(df_t), + sigma = c(se), lambda = c(0), stringsAsFactors = FALSE ) - x_l = c(low_eqt - qnorm(1-x$alpha)*points_l$sigma, - low_eqt + qnorm(1-x$alpha)*points_l$sigma) - x_u = c(high_eqt - qnorm(1-x$alpha)*points_l$sigma, - high_eqt + qnorm(1-x$alpha)*points_l$sigma) + # Calculate one-sided critical values + # For equivalence: lower bound uses right tail (greater), upper bound uses left tail (less) + # For MET: lower bound uses left tail (less), upper bound uses right tail (greater) + if (is_equivalence) { + crit_l_right = low_eqt + qnorm(1 - x$alpha) * se + crit_u_left = high_eqt - qnorm(1 - x$alpha) * se + } else { + crit_l_left = low_eqt - qnorm(1 - x$alpha) * se + crit_u_right = high_eqt + qnorm(1 - x$alpha) * se + } + # Build plot with one-sided rejection regions t_plot = ggplot(data = points, - aes_string(y = 0)) + - stat_dist_slab(data = points_l, - aes(fill = stat(x < x_l[1] | x > x_l[2]), - dist = dist_student_t( - mu = mu, - df = param, - sigma = sigma, - ncp = lambda - )), - alpha = .5, - # fill = NA, - slab_color = "black", - slab_size = .5) + - stat_dist_slab(data = points_u, - aes(fill = stat(x < x_u[1] | x > x_u[2]), - dist = dist_student_t( - mu = mu, - df = param, - sigma = sigma, - ncp = lambda - )), - - alpha = .5, - # fill = NA, - slab_color = "black", - slab_size = .5) + + aes(y = 0)) + + if (is_equivalence) { + t_plot = t_plot + + stat_dist_slab(data = points_l, + aes(fill = after_stat(x > crit_l_right), + dist = dist_student_t( + mu = mu, + df = param, + sigma = sigma, + ncp = lambda + )), + alpha = .5, + slab_color = "black", + slab_size = .5) + + stat_dist_slab(data = points_u, + aes(fill = after_stat(x < crit_u_left), + dist = dist_student_t( + mu = mu, + df = param, + sigma = sigma, + ncp = lambda + )), + alpha = .5, + slab_color = "black", + slab_size = .5) + } else { + t_plot = t_plot + + stat_dist_slab(data = points_l, + aes(fill = after_stat(x < crit_l_left), + dist = dist_student_t( + mu = mu, + df = param, + sigma = sigma, + ncp = lambda + )), + alpha = .5, + slab_color = "black", + slab_size = .5) + + stat_dist_slab(data = points_u, + aes(fill = after_stat(x > crit_u_right), + dist = dist_student_t( + mu = mu, + df = param, + sigma = sigma, + ncp = lambda + )), + alpha = .5, + slab_color = "black", + slab_size = .5) + } + + t_plot = t_plot + geom_point(data = data.frame(y = -.1, x = points$point), aes(x = x, y = y), @@ -675,15 +768,16 @@ plot.TOSTt <- function(x, xend = points$ci_high, y = -.1, yend = -.1, size = 1.5, - colour = "black")+ - # set palettes need true false + colour = "black") + scale_fill_manual(values = c("gray85", "green")) + geom_vline(aes(xintercept = low_eqt), linetype = "dashed") + geom_vline(aes(xintercept = high_eqt), linetype = "dashed") + - facet_wrap( ~ x_label) + - labs(caption = "Note: green indicates rejection region for null equivalence and MET hypotheses")+ + facet_wrap(~ x_label) + + labs(caption = paste0("Note: green indicates one-sided rejection region (", + ifelse(is_equivalence, "equivalence", "minimal effect"), + " test)")) + theme_tidybayes() + theme( legend.position = "none", @@ -697,14 +791,15 @@ plot.TOSTt <- function(x, axis.text.x = element_text(face = "bold", size = 11), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), - panel.background = element_rect(fill = "transparent",colour = NA), - plot.background = element_rect(fill = "transparent",colour = NA), - legend.background = element_rect(fill = "transparent",colour = NA) + panel.background = element_rect(fill = "transparent", colour = NA), + plot.background = element_rect(fill = "transparent", colour = NA), + legend.background = element_rect(fill = "transparent", colour = NA) ) + scale_x_continuous(sec.axis = dup_axis(breaks = c( round(low_eqt, round_t), round(high_eqt, round_t) ))) + return(t_plot) } diff --git a/man/TOSTt-methods.Rd b/man/TOSTt-methods.Rd index 3990f96..d9c0171 100644 --- a/man/TOSTt-methods.Rd +++ b/man/TOSTt-methods.Rd @@ -16,6 +16,7 @@ estimates = c("raw", "SMD"), ci_lines, ci_shades, + layout = c("stacked", "combined"), ... ) @@ -30,13 +31,15 @@ describe(x, ...) \item{...}{further arguments passed through, see description of return value for details..} -\item{type}{Type of plot to produce. Default is a consonance density plot "cd". Consonance plots (type = "cd") and null distribution plots (type = "tnull") can also be produced. Note: null distribution plots only available for estimates = "raw".} +\item{type}{Type of plot to produce. Default is "simple" which shows point estimates with confidence intervals. Other options include consonance plots ("c"), consonance density plots ("cd"), and null distribution plots ("tnull"). Note: null distribution plots only available for estimates = "raw".} \item{estimates}{indicator of what estimates to plot; options include "raw" or "SMD". Default is is both: c("raw","SMD").} \item{ci_lines}{Confidence interval lines for plots. Default is 1-alpha*2 (e.g., alpha = 0.05 is 90\%)} \item{ci_shades}{Confidence interval shades when plot type is "cd".} + +\item{layout}{Layout for displaying multiple estimates. Options are "stacked" (default, separate plots stacked vertically) or "combined" (single faceted plot with shared legend). Only applies when both "raw" and "SMD" are in estimates.} } \value{ \itemize{ diff --git a/man/as_htest.Rd b/man/as_htest.Rd index eb99eed..58d8bae 100644 --- a/man/as_htest.Rd +++ b/man/as_htest.Rd @@ -57,6 +57,7 @@ as_htest(res2) \seealso{ Other htest: \code{\link{htest-helpers}}, +\code{\link{plot_htest_est}()}, \code{\link{simple_htest}()} } \concept{htest} diff --git a/man/htest-helpers.Rd b/man/htest-helpers.Rd index 8e56aed..836e2dc 100644 --- a/man/htest-helpers.Rd +++ b/man/htest-helpers.Rd @@ -84,6 +84,7 @@ describe_htest(cor_result) \seealso{ Other htest: \code{\link{as_htest}()}, +\code{\link{plot_htest_est}()}, \code{\link{simple_htest}()} } \concept{htest} diff --git a/man/plot_htest_est.Rd b/man/plot_htest_est.Rd new file mode 100644 index 0000000..c0650f6 --- /dev/null +++ b/man/plot_htest_est.Rd @@ -0,0 +1,80 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/htest_helpers.R +\name{plot_htest_est} +\alias{plot_htest_est} +\title{Plot Estimate from 'htest' Object} +\usage{ +plot_htest_est(htest, alpha = NULL, describe = TRUE) +} +\arguments{ +\item{htest}{An S3 object of class 'htest' containing at minimum an \code{estimate} and +\code{conf.int} component. Examples include output from \code{t.test()}, \code{cor.test()}, +or TOSTER functions converted with \code{as_htest()}.} + +\item{alpha}{Significance level for determining the confidence level label.} + +\item{describe}{Logical. If TRUE (default), includes a concise statistical description +in the plot subtitle showing the test statistic, p-value, estimate, confidence interval, +and the null hypothesis.} +} +\value{ +A \code{ggplot} object that can be further customized using ggplot2 functions. +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#stable}{\figure{lifecycle-stable.svg}{options: alt='[Stable]'}}}{\strong{[Stable]}} + +Creates a simple point estimate plot with confidence interval from any 'htest' object +that contains an estimate and confidence interval. This provides a visual representation +of the effect size and its uncertainty, similar to a forest plot. +} +\details{ +The function creates a horizontal point-range plot showing: +\itemize{ +\item Point estimate (black dot) +\item Confidence interval (horizontal line) +\item Null value(s) as dashed vertical reference line(s) +} + +For two-sample t-tests, R's \code{t.test()} returns both group means as the estimate +rather than their difference. This function automatically computes the difference to display +a single meaningful estimate with its confidence interval. + +If the 'htest' object contains equivalence bounds (two values in \code{null.value}), +both bounds are displayed as dashed vertical lines. + +When \code{describe = TRUE}, the plot includes a three-line subtitle: +\enumerate{ +\item Test statistic and p-value +\item Point estimate and confidence interval +\item Null hypothesis statement +} +The method name appears as the plot title. +} +\examples{ +# Standard t-test +t_result <- t.test(extra ~ group, data = sleep) +plot_htest_est(t_result) + +# One-sample t-test +t_one <- t.test(sleep$extra, mu = 0) +plot_htest_est(t_one) + +# Correlation test +cor_result <- cor.test(mtcars$mpg, mtcars$wt) +plot_htest_est(cor_result) + +# TOST result converted to htest +tost_res <- t_TOST(extra ~ group, data = sleep, eqb = 1) +plot_htest_est(as_htest(tost_res)) + +# Without description +plot_htest_est(t_result, describe = FALSE) + +} +\seealso{ +Other htest: +\code{\link{as_htest}()}, +\code{\link{htest-helpers}}, +\code{\link{simple_htest}()} +} +\concept{htest} diff --git a/man/simple_htest.Rd b/man/simple_htest.Rd index a890959..4771664 100644 --- a/man/simple_htest.Rd +++ b/man/simple_htest.Rd @@ -171,11 +171,13 @@ Other TOST: Other htest: \code{\link{as_htest}()}, -\code{\link{htest-helpers}} +\code{\link{htest-helpers}}, +\code{\link{plot_htest_est}()} Other htest: \code{\link{as_htest}()}, -\code{\link{htest-helpers}} +\code{\link{htest-helpers}}, +\code{\link{plot_htest_est}()} } \concept{TOST} \concept{htest} diff --git a/tests/testthat/test-htest.R b/tests/testthat/test-htest.R index 124ccb1..1d400f9 100644 --- a/tests/testthat/test-htest.R +++ b/tests/testthat/test-htest.R @@ -656,3 +656,67 @@ test_that("All other htests",{ expect_equal(htest$p.value,df1$p.value) }) + +test_that("plot_htest_est works correctly", { + + # Standard two-sample t-test (should auto-convert to difference) + t_two <- t.test(extra ~ group, data = sleep) + p1 <- plot_htest_est(t_two) + expect_s3_class(p1, "ggplot") + + # One-sample t-test + t_one <- t.test(sleep$extra, mu = 0) + p2 <- plot_htest_est(t_one) + expect_s3_class(p2, "ggplot") + + # Correlation test + cor_res <- cor.test(mtcars$mpg, mtcars$wt) + p3 <- plot_htest_est(cor_res) + expect_s3_class(p3, "ggplot") + + # TOST converted to htest (equivalence bounds) + tost_res <- t_TOST(extra ~ group, data = sleep, eqb = 1) + htest_tost <- as_htest(tost_res) + p4 <- plot_htest_est(htest_tost) + expect_s3_class(p4, "ggplot") + + # Error cases + expect_error(plot_htest_est("not_htest"), + "Input must be an object of class") + + # htest without estimate + htest_no_est <- t_two + htest_no_est$estimate <- NULL + expect_error(plot_htest_est(htest_no_est), + "htest object has no estimate") + + # htest without conf.int + htest_no_ci <- t_two + htest_no_ci$conf.int <- NULL + expect_error(plot_htest_est(htest_no_ci), + "htest object has no confidence interval") + + # Wilcoxon test with CI + wilcox_res <- wilcox.test(extra ~ group, data = sleep, conf.int = TRUE) + p5 <- plot_htest_est(wilcox_res) + expect_s3_class(p5, "ggplot") + + # Test describe argument + p_desc <- plot_htest_est(t_two, describe = TRUE) + expect_s3_class(p_desc, "ggplot") + expect_true(!is.null(p_desc$labels$subtitle)) + expect_true(grepl("t\\(", p_desc$labels$subtitle)) # Should contain t statistic + + p_no_desc <- plot_htest_est(t_two, describe = FALSE) + expect_s3_class(p_no_desc, "ggplot") + expect_true(is.null(p_no_desc$labels$subtitle)) + + # Test with equivalence test (two null values) + res_eq <- simple_htest(x = sleep$extra[sleep$group == 1], + y = sleep$extra[sleep$group == 2], + paired = TRUE, mu = 1, alternative = "e") + p_eq <- plot_htest_est(res_eq) + expect_s3_class(p_eq, "ggplot") + expect_true(!is.null(p_eq$labels$subtitle)) + +}) diff --git a/tests/testthat/test-tTOST.R b/tests/testthat/test-tTOST.R index 93e5b2f..e042a41 100644 --- a/tests/testthat/test-tTOST.R +++ b/tests/testthat/test-tTOST.R @@ -1137,6 +1137,45 @@ test_that("plot generic function",{ }) +test_that("plot.TOSTt simple type layout options", { + # Test the new layout parameter and updated simple plot behavior + + test1 = t_TOST(extra ~ group, data = sleep, eqb = 2) + + # Test default stacked layout + p_stacked = plot(test1, type = "simple") + expect_true(inherits(p_stacked, c("gg", "ggplot")) || inherits(p_stacked, "gtable")) + + # Test combined layout + p_combined = plot(test1, type = "simple", layout = "combined") + expect_s3_class(p_combined, "ggplot") + + # Test stacked layout explicitly + p_stacked2 = plot(test1, type = "simple", layout = "stacked") + expect_true(inherits(p_stacked2, c("gg", "ggplot")) || inherits(p_stacked2, "gtable")) + + # Test with only raw estimates + p_raw = plot(test1, type = "simple", estimates = "raw") + expect_s3_class(p_raw, "ggplot") + + # Test with only SMD estimates + p_smd = plot(test1, type = "simple", estimates = "SMD") + expect_s3_class(p_smd, "ggplot") + + # Test combined layout with only one estimate type (should just return single plot) + p_raw_combined = plot(test1, type = "simple", estimates = "raw", layout = "combined") + expect_s3_class(p_raw_combined, "ggplot") + + # Test one-sample case + test_one = t_TOST(x = sleep$extra, eqb = 1) + p_one = plot(test_one, type = "simple") + expect_true(inherits(p_one, c("gg", "ggplot")) || inherits(p_one, "gtable")) + + p_one_combined = plot(test_one, type = "simple", layout = "combined") + expect_s3_class(p_one_combined, "ggplot") + +}) + test_that("Ensure paired output correct", { test1 = tsum_TOST(n1 = 23, diff --git a/vignettes/IntroTOSTt.R b/vignettes/IntroTOSTt.R index 0db2069..217fffd 100644 --- a/vignettes/IntroTOSTt.R +++ b/vignettes/IntroTOSTt.R @@ -163,7 +163,7 @@ print(res1) print(res1b) ## ----fig.width=6, fig.height=6------------------------------------------------ -plot(res1, type = "simple") +plot(res1, type = "simple", layout = "combined") ## ----fig.width=6, fig.height=6, eval=TRUE------------------------------------- # Shade the 90% and 95% CI areas diff --git a/vignettes/IntroTOSTt.Rmd b/vignettes/IntroTOSTt.Rmd index 86c3a39..7a526f2 100644 --- a/vignettes/IntroTOSTt.Rmd +++ b/vignettes/IntroTOSTt.Rmd @@ -251,7 +251,7 @@ One of the advantages of `t_TOST` is its built-in visualization capabilities. Th This is the default plot type, showing the point estimate and confidence intervals relative to the equivalence bounds: ```{r fig.width=6, fig.height=6} -plot(res1, type = "simple") +plot(res1, type = "simple", layout = "combined") ``` This plot clearly shows where our observed difference (with confidence intervals) falls in relation to our equivalence bounds (dashed vertical lines). diff --git a/vignettes/IntroTOSTt.html b/vignettes/IntroTOSTt.html index e13b698..a3694a0 100644 --- a/vignettes/IntroTOSTt.html +++ b/vignettes/IntroTOSTt.html @@ -585,8 +585,8 @@
This is the default plot type, showing the point estimate and confidence intervals relative to the equivalence bounds:
- -This plot clearly shows where our observed difference (with confidence intervals) falls in relation to our equivalence bounds (dashed vertical lines).
@@ -973,7 +973,7 @@The same visualization and description methods work with
tsum_TOST:
describe(res_tsum)
#> [1] "Using the One-sample t-test, a null hypothesis significance test (NHST), and a equivalence test, via two one-sided tests (TOST), were performed with an alpha-level of 0.05. These tested the null hypotheses that true mean is equal to 0 (NHST), and true mean is more extreme than 5.5 and 8.5 (TOST). The equivalence test was significant, t(149) = 5.078, p < 0.001 (mean = 5.843 90% C.I.[5.731, 5.955]; Hedges's g = 7.021 90% C.I.[6.327, 7.691]). At the desired error rate, it can be stated that the true mean is between 5.5 and 8.5."