d.quantile / app.R
WLenhard's picture
Upload app.R
5f09da6 verified
library(plumber)
#* @apiTitle Effect Size Calculator API
#' Calculate a Distribution-Free Effect Size (d_reg)
#'
#' This function computes a distribution-free effect size by modeling the
#' empirical distribution function (eCDF) of two groups via polynomial
#' regression. The effect size is computed as the standardized
#' difference between the means of the smoothed distributions.
#'
#' The method involves:
#' 1. Fitting polynomials to each group's quantile function: x = f(z)
#' 2. Computing moments (mean, variance) of the polynomials
#' 3. Calculating d(reg) using the pooled standard deviation
#'
#' @param x1 A numeric vector of data for the first group.
#' @param x2 A numeric vector of data for the second group.
#' @param degree The degree of the polynomial to fit (default = 5).
#' Higher degrees capture more complex distributional shapes but
#' may overfit with small samples.
#' @param CI Confidence level for confidence interval (default = NA, no CI computed).
#' If specified (e.g., 0.95), uses asymptotic normal approximation.
#' WARNING: CI formula assumes Cohen's d distribution and may not be accurate for d_reg.
#' @param silent Logical; if TRUE, suppresses warnings during fitting (default = TRUE).
#'
#' @return A list (S3 class "d_reg") containing:
#' \item{d_reg}{The distribution-free effect size (standardized mean difference).}
#' \item{group1_mean}{Mean of the smoothed distribution for group 1.}
#' \item{group1_variance}{Variance of the smoothed distribution for group 1.}
#' \item{group1_sd}{Standard deviation of the smoothed distribution for group 1.}
#' \item{group2_mean}{Mean of the smoothed distribution for group 2.}
#' \item{group2_variance}{Variance of the smoothed distribution for group 2.}
#' \item{group2_sd}{Standard deviation of the smoothed distribution for group 2.}
#' \item{pooled_sd}{Pooled standard deviation.}
#' \item{n1}{Sample size of group 1.}
#' \item{n2}{Sample size of group 2.}
#' \item{model1}{Fitted polynomial model for group 1.}
#' \item{model2}{Fitted polynomial model for group 2.}
#' \item{default_degree}{Polynomial degree used.}
#' \item{tie_proportion_1}{Proportion of tied values in group 1.}
#' \item{tie_proportion_2}{Proportion of tied values in group 2.}
#' \item{n_unique_1}{Number of unique values in group 1.}
#' \item{n_unique_2}{Number of unique values in group 2.}
#' \item{ci_lower}{Lower bound of confidence interval (if CI specified).}
#' \item{ci_upper}{Upper bound of confidence interval (if CI specified).}
#' \item{ci_level}{Confidence level (if CI specified).}
#'
#' @details
#' The method is distribution-free and converges to Cohen's d under normality with
#' increasing group size. It is robust to outliers and skewness compared to
#' classical parametric methods.
#'
#' Sample size requirements: At least (degree + 1) observations per group.
#' Recommended: n > 10 per group for stable polynomial fits.
#' For small samples (n < 20), consider using degree = 3 or lower.
#'
#' Confidence intervals use an asymptotic approximation based on Cohen's d
#' distribution and may not accurately reflect the true sampling distribution
#' of d_reg, especially in small samples or non-normal data.
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' @references
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen's d.
#'
#' @examples
#' # Normal distributions
#' set.seed(123)
#' x1 <- rnorm(30, mean = 0, sd = 1)
#' x2 <- rnorm(30, mean = 0.5, sd = 1)
#' result <- d.reg(x1, x2)
#' print(result)
#'
#' # With confidence interval
#' result_ci <- d.reg(x1, x2, CI = 0.95)
#' print(result_ci)
#'
#' # Skewed distributions
#' x1 <- rexp(50, rate = 1)
#' x2 <- rexp(50, rate = 0.8)
#' result <- d.reg(x1, x2, degree = 4)
#' print(result)
#'
#' @export
d.reg <- function(x1, x2, degree = 5, CI = NA, silent = TRUE) {
# ============================================================================
# Input Validation
# ============================================================================
if (!is.numeric(x1) || !is.numeric(x2)) {
stop("Both x1 and x2 must be numeric vectors.")
}
# Handle missing values
if (any(is.na(x1)) || any(is.na(x2))) {
if (!silent) {
warning("Missing values detected and will be removed.")
}
x1 <- x1[!is.na(x1)]
x2 <- x2[!is.na(x2)]
}
n1 <- length(x1)
n2 <- length(x2)
# Check for empty groups
if (n1 == 0 || n2 == 0) {
stop("Cannot compute effect size with empty groups after removing NAs.")
}
# Check sufficient sample size for polynomial degree
if (n1 < degree + 1) {
stop("Group 1 has insufficient data: need at least ", degree + 1,
" observations for degree ", degree, " polynomial (got ", n1, ").")
}
if (n2 < degree + 1) {
stop("Group 2 has insufficient data: need at least ", degree + 1,
" observations for degree ", degree, " polynomial (got ", n2, ").")
}
# Validate CI parameter if provided
if (!is.na(CI)) {
if (!is.numeric(CI) || length(CI) != 1) {
stop("CI must be a single numeric value or NA.")
}
if (CI <= 0 || CI >= 1) {
stop("CI must be between 0 and 1 (exclusive).")
}
}
model1 <- fit_polynomial(x1, degree)
model2 <- fit_polynomial(x2, degree)
# Extract tie information
tie1 <- attr(model1, "tie_proportion")
tie2 <- attr(model2, "tie_proportion")
n_unique1 <- attr(model1, "n_unique")
n_unique2 <- attr(model2, "n_unique")
# Warn about substantial ties
if (!silent && (tie1 > 0.3 || tie2 > 0.3)) {
message(sprintf(
"Note: Substantial ties detected (Group 1: %.1f%%, Group 2: %.1f%%).",
tie1 * 100, tie2 * 100
))
message("This suggests discrete/ordinal data. Results should be interpreted cautiously.")
message("Consider comparing multiple effect size measures for discrete data.")
}
moments1 <- get_moments(model1, group_label = "Group 1")
moments2 <- get_moments(model2, group_label = "Group 2")
# Weighted pooled variance (population formula, not sample formula)
weighted_pooled_variance <- (n1 * moments1$variance + n2 * moments2$variance) / (n1 + n2)
pooled_sd <- sqrt(weighted_pooled_variance)
mean_diff <- moments2$mean - moments1$mean
# Handle edge cases
if (pooled_sd == 0) {
if (mean_diff == 0) {
d_reg <- 0
} else {
d_reg <- sign(mean_diff) * Inf
if (!silent) {
warning("Pooled SD is zero but means differ. Returning Inf with appropriate sign.")
}
}
} else {
d_reg <- mean_diff / pooled_sd
}
result <- list(
d_reg = d_reg,
# Group 1 statistics
group1_mean = moments1$mean,
group1_variance = moments1$variance,
group1_sd = sqrt(moments1$variance),
# Group 2 statistics
group2_mean = moments2$mean,
group2_variance = moments2$variance,
group2_sd = sqrt(moments2$variance),
# Pooled statistics
pooled_sd = pooled_sd,
# Sample sizes
n1 = n1,
n2 = n2,
# Models
model1 = model1,
model2 = model2,
# Metadata
default_degree = degree,
tie_proportion_1 = tie1,
tie_proportion_2 = tie2,
n_unique_1 = n_unique1,
n_unique_2 = n_unique2
)
if (!is.na(CI)) {
# Standard error using asymptotic approximation
# NOTE: This formula assumes Cohen's d distribution and may not be
# accurate for d_reg, especially in small samples or non-normal data
se_dreg <- sqrt((n1 + n2) / (n1 * n2) + (d_reg^2) / (2 * (n1 + n2)))
# Degrees of freedom
df <- n1 + n2 - 2
# Critical value from t-distribution
alpha <- 1 - CI
t_crit <- qt(1 - alpha / 2, df)
# Confidence interval bounds
ci_lower <- d_reg - t_crit * se_dreg
ci_upper <- d_reg + t_crit * se_dreg
# Add to result
result$ci_lower <- ci_lower
result$ci_upper <- ci_upper
result$ci_level <- CI
result$ci_se <- se_dreg
result$ci_df <- df
}
class(result) <- "d_reg"
return(result)
}
#' Fit a Polynomial to eCDF
#'
#' This helper function fits a polynomial regression model to represent the
#' distribution. It models the relationship between
#' z-scores and observed raw scores.
#'
#' @param x A numeric vector of observations.
#' @param poly_degree The degree of the polynomial to fit.
#' @param check_monotonicity Logical; should monotonicity be enforced by
#' reducing polynomial degree if needed? (default = FALSE for speed)
#' @param min_degree Minimum polynomial degree to try (default = 1, representing
#' a linear fit to a normal distribution).
#'
#' @return An lm model object representing x = f(z), where z ~ N(0,1).
#' Additional attributes:
#' \describe{
#' \item{sample_size}{Original sample size}
#' \item{n_unique}{Number of unique values (for tie detection)}
#' \item{tie_proportion}{Proportion of tied observations}
#' \item{poly_degree}{Actual polynomial degree used (may be reduced)}
#' \item{monotonic}{Logical; is the fitted function monotonic?}
#' \item{degree_reduced}{Logical; was degree reduced from requested?}
#' }
#'
#' @details
#' The function uses average ranks (midrank method) to handle tied observations,
#' which is the standard approach in rank-based statistics. Plotting positions
#' (rank - 0.5)/n avoid infinite z-scores at boundaries.
#'
#' When substantial ties are present (>10% of observations), the function may
#' automatically reduce the polynomial degree to avoid overfitting to a small
#' number of unique values.
#'
#' If check_monotonicity=TRUE, the function iteratively reduces the polynomial
#' degree until a monotonic fit is achieved or min_degree is reached.
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' Licensed under the MIT License
#'
#' Citation:
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen’s d.
#'
#' @export
fit_polynomial <- function(x, poly_degree,
check_monotonicity = TRUE,
min_degree = 1) {
# Step 1: Input validation and tie detection
n <- length(x)
if (n < 3) {
stop("Need at least 3 observations to fit a polynomial.")
}
# Count unique values to detect ties
n_unique <- length(unique(x))
tie_proportion <- 1 - (n_unique / n)
# Step 2: Adjust polynomial degree based on unique values
# Can't fit more parameters than unique data points
max_possible_degree <- n_unique - 1
if (poly_degree > max_possible_degree) {
warning(sprintf(
"Requested polynomial degree (%d) exceeds number of unique values (%d). ",
poly_degree, n_unique,
"Reducing to degree %d."
), max_possible_degree)
poly_degree <- max_possible_degree
}
# Additional reduction for substantial ties
if (tie_proportion > 0.3 && poly_degree > 3) {
# With >30% ties, be more conservative
recommended_degree <- min(poly_degree, max(3, floor(n_unique / 2)))
if (recommended_degree < poly_degree) {
warning(sprintf(
"High proportion of ties (%.1f%%). Reducing polynomial degree from %d to %d for stability.",
tie_proportion * 100, poly_degree, recommended_degree
))
poly_degree <- recommended_degree
}
}
# Ensure we stay above minimum
if (poly_degree < min_degree) {
stop(sprintf(
"Insufficient unique values (%d) to fit minimum polynomial degree (%d). ",
n_unique, min_degree,
"Need at least %d unique observations."
), min_degree + 1)
}
# Step 3: Compute ranks and z-scores (handles ties via midrank)
# Average ranks handle ties by assigning mean rank to tied observations
avg_ranks <- rank(x, ties.method = "average")
# Convert ranks to plotting positions
p <- (avg_ranks - 0.5) / n
# Transform to standard normal quantiles
z <- qnorm(p)
# Step 4: Fit polynomial, with optional monotonicity enforcement
check_range <- range(z)
current_degree <- poly_degree
degree_reduced <- FALSE
monotonic <- FALSE
if (check_monotonicity) {
while (current_degree >= min_degree) {
model <- lm(x ~ poly(z, current_degree, raw = TRUE))
check <- check_monotonicity(model, z_range = check_range)
if (check$is_monotonic) {
monotonic <- TRUE
break
}
# Reduce degree and try again
current_degree <- current_degree - 1
degree_reduced <- TRUE
}
# Emergency fallback
if (current_degree < min_degree) {
current_degree <- min_degree
# Fit linear even if non-monotonic (rare/impossible for degree 1 unless negative correlation)
model <- lm(x ~ poly(z, current_degree, raw = TRUE))
check <- check_monotonicity(model, z_range = check_range)
monotonic <- check$is_monotonic
}
} else {
model <- lm(x ~ poly(z, current_degree, raw = TRUE))
check <- check_monotonicity(model, z_range = check_range)
monotonic <- check$is_monotonic
}
# Metadata
attr(model, "sample_size") <- n
attr(model, "n_unique") <- n_unique # ADD THIS
attr(model, "tie_proportion") <- tie_proportion # ADD THIS
attr(model, "poly_degree") <- current_degree
attr(model, "monotonic") <- monotonic
attr(model, "min_derivative") <- check$min_derivative
return(model)
}
#' Check Monotonicity of Fitted Quantile Function
#'
#' Analytically checks if a polynomial quantile function is monotonic
#' within the observed range of the data.
#'
#' @param model An lm model object fitted with poly(..., raw=TRUE).
#' @param z_range A numeric vector of length 2 defining the range [min, max]
#' over which to check monotonicity. If NULL, checks reasonable defaults
#' based on N (-4 to 4).
#' @param strictly_positive Logical; if TRUE, derivative must be > 0 (strict).
#' If FALSE, derivative can be >= 0 (allows flat regions).
#'
#' @return A list containing:
#' \item{is_monotonic}{Logical; TRUE if monotonic in range.}
#' \item{min_derivative}{The lowest slope found in the range.}
#' \item{location_min}{The z-value where the minimum slope occurs.}
#'
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' Licensed under the MIT License
#'
#' Citation:
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen’s d.
#'
#' @export
check_monotonicity <- function(model, z_range = c(-4, 4),
strictly_positive = FALSE) {
# Input handling
if (!inherits(model, "lm")) stop("model must be an lm object")
if (length(z_range) != 2 || z_range[1] >= z_range[2]) {
stop("z_range must be a vector [min, max] with min < max")
}
# Extract polynomial coefficients
coeffs <- coef(model)
# Handle NAs (rare cases where regression had failed)
if (any(is.na(coeffs))) return(list(
is_monotonic = FALSE,
min_derivative = -Inf,
location_min = NA
))
degree <- length(coeffs) - 1
# Special cases for low-degree polynomials
if (degree == 0) {
# CASE: Constant (Degree 0)
return(list(
is_monotonic = TRUE,
min_derivative = 0,
location_min = 0
))
} else if (degree == 1) {
# CASE: Linear (Degree 1)
# f(z) = b0 + b1*z -> f'(z) = b1
slope <- coeffs[2]
return(list(
is_monotonic = if(strictly_positive) slope > 0 else slope >= 0,
min_derivative = slope,
location_min = 0
))
}
# 1. Calculate coefficients of First Derivative f'(z)
# f(z) = c0 + c1*z + c2*z^2 + c3*z^3 ...
# f'(z) = c1 + 2*c2*z + 3*c3*z^2 ...
deriv1_coeffs <- numeric(degree) # Degree drops by 1
for (i in 1:degree) {
# Coeff index i+1 corresponds to power z^i in original model
deriv1_coeffs[i] <- coeffs[i + 1] * i
}
# Define function to evaluate slope at specific z values
eval_deriv <- function(z, coefs) {
# Horner's method
val <- coefs[length(coefs)]
if (length(coefs) > 1) {
for (i in (length(coefs)-1):1) {
val <- val * z + coefs[i]
}
}
return(val)
}
# 2. Find Critical Points of the Derivative
# To find where slope is minimized, we look for roots of f''(z)
# Coefficients of f''(z)
degree_d1 <- degree - 1
if (degree_d1 >= 1) {
deriv2_coeffs <- numeric(degree_d1)
for (i in 1:degree_d1) {
deriv2_coeffs[i] <- deriv1_coeffs[i + 1] * i
}
# Find roots (complex) of f''(z)
roots_complex <- polyroot(deriv2_coeffs)
# Filter for real roots within the z_range
# Keep if imaginary part is negligible
real_indices <- abs(Im(roots_complex)) < 1e-9
roots_real <- Re(roots_complex)[real_indices]
# Filter roots strictly inside our check range
critical_z <- roots_real[roots_real >= z_range[1] & roots_real <= z_range[2]]
} else {
# If derivative is constant (should be handled by degree=1 check above)
critical_z <- numeric(0)
}
# 3. Check Constraints (Boundaries + Critical Points)
# The minimum slope MUST occur either at endpoints or at a local extremum
check_points <- unique(c(z_range[1], z_range[2], critical_z))
slopes <- sapply(check_points, eval_deriv, coefs = deriv1_coeffs)
min_slope <- min(slopes)
loc_min <- check_points[which.min(slopes)]
threshold <- if(strictly_positive) 1e-9 else -1e-9
return(list(
is_monotonic = min_slope >= threshold,
min_derivative = min_slope,
location_min = loc_min
))
}
#' Calculate Moments from a Fitted Polynomial Function
#'
#' This function computes the mean and variance of the distribution represented
#' by a polynomial function using Iserlis (1918) theorem.
#'
#' @param model An lm model object from \code{\link{fit_quantile_function}()}.
#' The model should represent the relationship x = f(z) where z are standard
#' normal quantiles and x are the observed values.
#' @param group_label Optional character string label for warning messages
#' (default = "Unknown"). Used to identify which group produced warnings in
#' multi-group comparisons.
#'
#' @return A list with elements:
#' \item{mean}{The expected value E[X] where X = f(Z), Z ~ N(0,1).}
#' \item{variance}{The variance Var(X) = E[X²] - (E[X])².}
#'
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' Licensed under the MIT License
#'
#' Citation:
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen’s d.
#'
#' @seealso
#' \code{\link{fit_polynomial}} for fitting the polynomial model.
#' \code{\link{d.reg}} for the main effect size calculation.
#'
#' @examples
#' # Generate sample data
#' set.seed(123)
#' x <- rnorm(50, mean = 100, sd = 15)
#'
#' # Fit quantile function
#' n <- length(x)
#' avg_ranks <- rank(x, ties.method = "average")
#' p <- (avg_ranks - 0.5) / n
#' z <- qnorm(p)
#' model <- lm(x ~ poly(z, 5, raw = TRUE))
#'
#' # Compute moments
#' moments <- get_moments(model, group_label = "Test Group")
#'
#' cat("Mean:", moments$mean, "\n")
#' cat("Variance:", moments$variance, "\n")
#' cat("SD:", sqrt(moments$variance), "\n")
#'
#' # Compare with sample statistics
#' cat("\nSample mean:", mean(x), "\n")
#' cat("Sample variance:", var(x), "\n")
#'
#'
#'
#' @export
get_moments <- function(model, group_label = "Unknown") {
# Extract coefficients and determine polynomial degree
coeffs <- coef(model)
k <- length(coeffs) - 1 # polynomial degree
# Pre-compute standard normal raw moments: E[Z^j]
# For j even: E[Z^j] = (j-1)!! = (j-1) × (j-3) × ... × 3 × 1
# For j odd: E[Z^j] = 0 (due to symmetry)
compute_moment <- function(j) {
if (j == 0) return(1) # E[Z^0] = 1 (total probability)
if (j %% 2 == 1) return(0) # Odd moments vanish
# Even moments: double factorial
# E[Z^2] = 1, E[Z^4] = 3, E[Z^6] = 15, E[Z^8] = 105, ...
result <- 1
for (i in seq(j - 1, 1, by = -2)) {
result <- result * i
}
return(result)
}
# We need moments up to degree 2k for computing E[X^2]
max_moment <- 2 * k
moments_z <- sapply(0:max_moment, compute_moment)
# Compute mean: μ = E[X] = E[f(Z)] = Σ β_j E[Z^j]
# Only even-powered terms contribute due to symmetry
mu <- 0
for (j in 0:k) {
mu <- mu + coeffs[j + 1] * moments_z[j + 1]
}
# Compute variance: σ² = E[X²] - μ²
# First calculate E[X²] = E[(Σ β_i Z^i)²] = Σ_i Σ_j β_i β_j E[Z^(i+j)]
E_X2 <- 0
for (i in 0:k) {
for (j in 0:k) {
power <- i + j
if (power <= max_moment) {
E_X2 <- E_X2 + coeffs[i + 1] * coeffs[j + 1] * moments_z[power + 1]
}
}
}
variance <- E_X2 - mu^2
# Handle numerical edge cases
if (variance < 0) {
if (abs(variance) < 1e-10) {
# Likely just numerical noise - round to zero
variance <- 0
} else {
# Substantial negative variance indicates a real problem
warning(
"Variance for ", group_label, " is negative (",
format(variance, scientific = TRUE, digits = 3),
"). This indicates numerical instability in the polynomial fit. ",
"Consider reducing the polynomial degree or checking for data issues.",
call. = FALSE
)
# Set to zero to avoid downstream errors, but flag it
variance <- 0
}
}
return(list(
mean = mu,
variance = variance
))
}
#' Print Method for d_reg Objects
#'
#' @param x An object of class "d_reg"
#' @param ... Additional arguments (not used)
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' Licensed under the MIT License
#'
#' Citation:
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen’s d.
#'
#' @export
print.d_reg <- function(x, ...) {
cat("\nDistribution-Free Effect Size (d_reg)\n")
cat("===================================\n\n")
cat("Effect size d_reg:", round(x$d_reg, 4), "\n")
# Display CI if available
if (!is.null(x$ci_lower)) {
cat(sprintf("%d%% CI: [%.4f, %.4f]\n",
round(x$ci_level * 100),
x$ci_lower,
x$ci_upper), "\n")
}
cat("\nGroup 1: n =", x$n1, ", mean =", round(x$group1_mean, 4),
", SD =", round(x$group1_sd, 4), "\n")
cat("Group 2: n =", x$n2, ", mean =", round(x$group2_mean, 4),
", SD =", round(x$group2_sd, 4), "\n")
cat("Pooled SD:", round(x$pooled_sd, 4), "\n")
cat("Polynomial degree:", x$default_degree, "\n")
invisible(x)
}
#' Summary Method for d_reg Objects
#'
#' Provides detailed summary statistics and diagnostic information.
#'
#' @param object An object of class "d_reg"
#'
#' @author Wolfgang Lenhard and Alexandra Lenhard
#' Licensed under the MIT License
#'
#' Citation:
#' Lenhard, W. & Lenhard, A. (submitted). Distribution-Free Effect Size Estimation:
#' A Robust Alternative to Cohen’s d.
#'
#' @export
summary.d_reg <- function(object, ...) {
cat("\n")
cat("=======================================================\n")
cat(" Distribution-Free Effect Size Analysis (d_reg)\n")
cat("=======================================================\n\n")
cat("Effect Size:\n")
cat(" d_reg =", round(object$d_reg, 4), "\n")
# Interpretation
abs_d <- abs(object$d_reg)
interpretation <- if (abs_d < 0.2) {
"negligible"
} else if (abs_d < 0.5) {
"small"
} else if (abs_d < 0.8) {
"medium"
} else {
"large"
}
cat(" Interpretation:", interpretation, "\n\n")
cat("Group 1:\n")
cat(" Sample size: ", object$n1, "\n")
cat(" Mean (smoothed): ", round(object$group1_mean, 4), "\n")
cat(" SD (smoothed): ", round(object$group1_sd, 4), "\n")
cat(" Variance: ", round(object$group1_variance, 4), "\n\n")
cat("Group 2:\n")
cat(" Sample size: ", object$n2, "\n")
cat(" Mean (smoothed): ", round(object$group2_mean, 4), "\n")
cat(" SD (smoothed): ", round(object$group2_sd, 4), "\n")
cat(" Variance: ", round(object$group2_variance, 4), "\n\n")
cat("Pooled Statistics:\n")
cat(" Pooled SD: ", round(object$pooled_sd, 4), "\n")
cat(" Mean difference: ", round(object$group2_mean - object$group1_mean, 4), "\n\n")
cat("Model Details:\n")
cat(" Polynomial degree:", object$default_degree, "\n")
cat(" Model 1 R²: ", round(summary(object$model1)$r.squared, 4), "\n")
cat(" Model 2 R²: ", round(summary(object$model2)$r.squared, 4), "\n\n")
if(!is.null(object$ci_lower) && !is.null(object$ci_upper)) {
cat(sprintf("Confidence Interval (%.1f%%): [%.4f, %.4f]\n\n",
object$ci_level * 100,
round(object$ci_lower, 4),
round(object$ci_upper, 4)))
}
cat("=======================================================\n\n")
invisible(object)
}
# API endpoint
#* Calculate effect size from two groups
#* @param group1 Comma-separated numeric values for group 1
#* @param group2 Comma-separated numeric values for group 2
#* @param ci Confidence interval level (default 0.95)
#* @post /calculate
#* @get /calculate
function(group1 = NULL, group2 = NULL, degree = 4, ci = 0.95) {
tryCatch({
# Check if parameters are missing
if (is.null(group1) || is.null(group2) || group1 == "" || group2 == "") {
return(list(
success = FALSE,
error = "Missing required parameters: group1 and group2"
))
}
# Parse input
x1 <- as.numeric(unlist(strsplit(as.character(group1), ",")))
x2 <- as.numeric(unlist(strsplit(as.character(group2), ",")))
ci_level <- as.numeric(ci)
degree <- as.numeric(degree)
# Remove any NA values from parsing
x1 <- x1[!is.na(x1)]
x2 <- x2[!is.na(x2)]
if (length(x1) == 0 || length(x2) == 0) {
return(list(
success = FALSE,
error = "Invalid input: Could not parse numeric values from input strings"
))
}
# Calculate effect size with CI
result <- d.reg(x1, x2, degree, CI = ci_level, silent = TRUE)
# Return clean result
return(list(
success = TRUE,
d_reg = result$d_reg,
ci_lower = result$ci_lower,
ci_upper = result$ci_upper,
ci_level = result$ci_level,
group1 = list(
n = result$n1,
mean = result$group1_mean,
sd = result$group1_sd,
mean_classic = mean(x1),
sd_classic = sd(x1)
),
group2 = list(
n = result$n2,
mean = result$group2_mean,
sd = result$group2_sd,
mean_classic = mean(x2),
sd_classic = sd(x2)
),
pooled_sd = result$pooled_sd,
degree = result$default_degree
))
}, error = function(e) {
return(list(
success = FALSE,
error = as.character(e$message)
))
})
}
#* Health check endpoint
#* @get /
function() {
return(list(
status = "running",
message = "Effect Size Calculator API is ready",
endpoints = list(
calculate = "/calculate?group1=1,2,3&group2=4,5,6"
)
))
}