Background and Rationale

This document presents a power analysis for examining appointment wait time differences between private equity-owned (PE) physicians vs. physicians that primarily accept Blue Cross/Blue Shield (BCBS) insurance. Unlike the original Corbisiero et al. (2023) study, we’re exploring a scenario where PE-owned physicians have shorter wait times than BCBS-primary physicians.

Study Parameters

wait_time_pe <- 12.5
wait_time_bcbs <- 15.8

subspecialties <- c("General OB/GYN", "Urogynecology", "Gynecologic Oncology", 
                   "Reproductive Endocrinology", "Maternal-Fetal Medicine")

wait_time_pe_subspec <- c(10.2, 11.5, 9.8, 12.3, 10.0)
wait_time_bcbs_subspec <- c(14.0, 16.0, 15.0, 17.0, 14.5)

sd_between <- 6.0
sd_within <- 4.0
total_sd <- sqrt(sd_between^2 + sd_within^2)
logger::log_info("Total standard deviation: {round(total_sd, 2)} days")

alpha <- 0.05
power_target <- 0.80
dropout_rate <- 0.15

Expected Wait Times by Ownership Structure

ownership_wait_times <- data.frame(
  Ownership_Type = c("PE-Owned", "BCBS-Primary"),
  Mean_Wait_Days = c(wait_time_pe, wait_time_bcbs),
  Difference = c(NA, wait_time_pe - wait_time_bcbs)
)

knitr::kable(ownership_wait_times,
             caption = "Expected Wait Times (in Business Days) by Physician Ownership Type",
             col.names = c("Physician Type", "Mean Wait Time (Days)", "Difference from PE-Owned"),
             align = "lrr") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                            full_width = FALSE) %>%
  kableExtra::row_spec(2, bold = TRUE)
Expected Wait Times (in Business Days) by Physician Ownership Type
Physician Type Mean Wait Time (Days) Difference from PE-Owned
PE-Owned 12.5 NA
BCBS-Primary 15.8 -3.3

Expected Wait Times by Subspecialty and Ownership Type

subspecialty_wait_times <- data.frame(
  Subspecialty = subspecialties,
  PE_Wait = wait_time_pe_subspec,
  BCBS_Wait = wait_time_bcbs_subspec
) %>%
  mutate(
    Difference = BCBS_Wait - PE_Wait,
    Ratio = round(BCBS_Wait / PE_Wait, 2)
  )

knitr::kable(subspecialty_wait_times,
             caption = "Expected Wait Times (in Business Days) by OB/GYN Subspecialty and Ownership Type",
             col.names = c("Subspecialty", "PE Wait", "BCBS Wait", "Difference (BCBS - PE)", "Ratio (BCBS/PE)"),
             align = "lrrrr",
             digits = c(0, 1, 1, 1, 2)) %>%
  kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                            full_width = FALSE)
Expected Wait Times (in Business Days) by OB/GYN Subspecialty and Ownership Type
Subspecialty PE Wait BCBS Wait Difference (BCBS - PE) Ratio (BCBS/PE)
General OB/GYN 10.2 14.0 3.8 1.37
Urogynecology 11.5 16.0 4.5 1.39
Gynecologic Oncology 9.8 15.0 5.2 1.53
Reproductive Endocrinology 12.3 17.0 4.7 1.38
Maternal-Fetal Medicine 10.0 14.5 4.5 1.45
subspecialty_df <- data.frame(
  Subspecialty = rep(subspecialties, each = 2),
  Ownership = rep(c("PE-Owned", "BCBS-Primary"), times = length(subspecialties)),
  Wait_Days = c(rbind(wait_time_pe_subspec, wait_time_bcbs_subspec))
)

ggplot(subspecialty_df, aes(x = Subspecialty, y = Wait_Days, fill = Ownership)) +
  geom_bar(stat = "identity", position = position_dodge(width = 0.9), width = 0.8) +
  geom_text(aes(label = round(Wait_Days, 1)), position = position_dodge(width = 0.9), 
            vjust = -0.5, size = 3.5) +
  scale_fill_manual(values = c("PE-Owned" = "#28a745", "BCBS-Primary" = "#3366CC")) +
  labs(title = "Expected Wait Times by Subspecialty and Ownership Type",
       subtitle = "PE-Owned physicians shown with shorter wait times than BCBS-Primary",
       x = "Subspecialty",
       y = "Wait Time (Business Days)") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
        axis.title = element_text(size = 14),
        plot.title = element_text(size = 16, face = "bold"),
        panel.grid.major.x = element_blank())

Effect Size Calculations and Sample Size Estimations

mean_diff_overall <- wait_time_bcbs - wait_time_pe
cohen_d_overall <- round(abs(mean_diff_overall / total_sd), 2)

ownership_effect_size <- data.frame(
  Comparison = "BCBS-Primary vs. PE-Owned",
  Mean_Difference = mean_diff_overall,
  Effect_Size = cohen_d_overall,
  Interpretation = ifelse(cohen_d_overall < 0.5, "Medium", "Large")
)

knitr::kable(ownership_effect_size,
             caption = "Effect Size Calculation for Ownership Comparison",
             col.names = c("Comparison", "Mean Difference (Days)", "Cohen's d", "Interpretation"),
             align = "lrrc",
             digits = c(0, 1, 2, 0)) %>%
  kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                            full_width = FALSE)
Effect Size Calculation for Ownership Comparison
Comparison Mean Difference (Days) Cohen’s d Interpretation
BCBS-Primary vs. PE-Owned 3.3 0.46 Medium

Power Curve

effect_sizes_range <- seq(0.2, 1.6, by = 0.2)
sample_sizes_range <- seq(10, 50, by = 2)
power_grid <- expand.grid(effect_size = effect_sizes_range, sample_size = sample_sizes_range)

power_grid$power <- mapply(function(d, n) {
  pwr.t.test(d = d, n = n, sig.level = alpha, type = "two.sample")$power
}, power_grid$effect_size, power_grid$sample_size)

power_grid$label <- paste0("d = ", power_grid$effect_size, "   (", round(power_grid$effect_size * total_sd, 1), " days)")

ggplot(power_grid, aes(x = sample_size, y = power, color = label)) +
  geom_line(size = 1) +
  geom_hline(yintercept = power_target, linetype = "dashed", color = "red") +
  scale_color_viridis_d(name = "Effect Size\n(Cohen's d and Days)") +
  scale_x_continuous(breaks = seq(10, 50, by = 2)) +
  scale_y_continuous(limits = c(0.5, 1.0)) +
  labs(title = "Power Curve for Wait Time Comparison",
       x = "Sample Size (per group)",
       y = "Statistical Power",
       caption = "Red dashed line: target power (80%)") +
  theme_minimal()

Interactive Power Curve

library(plotly)

effect_sizes_range <- seq(0.2, 1.6, by = 0.2)
sample_sizes_range <- seq(10, 50, by = 2)
power_grid <- expand.grid(effect_size = effect_sizes_range, sample_size = sample_sizes_range)

power_grid$power <- mapply(function(d, n) {
  pwr::pwr.t.test(d = d, n = n, sig.level = alpha, type = "two.sample")$power
}, power_grid$effect_size, power_grid$sample_size)

power_grid$label <- paste0("d = ", power_grid$effect_size, "   (", round(power_grid$effect_size * total_sd, 1), " days)")

static_plot <- ggplot2::ggplot(power_grid, ggplot2::aes(x = sample_size, y = power, color = label)) +
  ggplot2::geom_line(size = 1) +
  ggplot2::geom_hline(yintercept = power_target, linetype = "dashed", color = "red") +
  ggplot2::scale_color_viridis_d(name = "Effect Size\n(Cohen's d and Days)") +
  ggplot2::scale_x_continuous(breaks = seq(10, 50, by = 2)) +
  ggplot2::scale_y_continuous(limits = c(0.5, 1.0)) +
  ggplot2::labs(
    title = "Interactive Power Curve for Wait Time Comparison",
    x = "Sample Size (per group)",
    y = "Statistical Power",
    caption = "Red dashed line: target power (80%)"
  ) +
  ggplot2::theme_minimal()

plotly::ggplotly(static_plot)
## Final Sample Size Determination
overall_power_analysis <- pwr.t.test(
  d = cohen_d_overall,
  sig.level = alpha,
  power = power_target,
  type = "two.sample",
  alternative = "two.sided"
)

n_per_group <- ceiling(overall_power_analysis$n)
total_required <- n_per_group * 2
adjusted_total <- ceiling(total_required / (1 - dropout_rate))

final_summary <- data.frame(
  Description = c(
    "Cohen's d",
    "Sample size per group (unadjusted)",
    "Total required (unadjusted)",
    "Dropout adjustment (15%)",
    "Total required (adjusted)"
  ),
  Value = c(
    cohen_d_overall,
    n_per_group,
    total_required,
    "15%",
    adjusted_total
  )
)

knitr::kable(final_summary,
             caption = "Final Sample Size Determination",
             col.names = c("Description", "Value"),
             align = "lc") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                            full_width = FALSE) %>%
  kableExtra::row_spec(2:5, bold = TRUE, background = "#e6f0ff")
Final Sample Size Determination
Description Value
Cohen’s d 0.46
Sample size per group (unadjusted) 76
Total required (unadjusted) 152
Dropout adjustment (15%) 15%
Total required (adjusted) 179

Conclusion

cat("\nThis power analysis scenario explores reduced wait times for PE-owned physicians compared to BCBS-primary physicians. \n")
## 
## This power analysis scenario explores reduced wait times for PE-owned physicians compared to BCBS-primary physicians.
cat(paste0("With a Cohen's d of ", cohen_d_overall, ", the required sample size per group for 80% power is ", n_per_group, ".\n"))
## With a Cohen's d of 0.46, the required sample size per group for 80% power is 76.
cat(paste0("After accounting for a 15% dropout rate, the total sample size needed is ", adjusted_total, ".\n"))
## After accounting for a 15% dropout rate, the total sample size needed is 179.