1 Generación del Dataset Clínico Simulado

En esta sección se crea un ensayo clínico simulado, utilizando clinical_data() del paquete biostats, tal como se recomienda en su documentación oficial. Este diseño se asemeja a un estudio con tres brazos paralelos y visitas repetidas, con pérdidas y datos faltantes, lo cual refleja escenarios reales descritos en ICH E9 Statistical Principles for Clinical Trials y Pocock (Clinical Trials: A Practical Approach).

set.seed(2025)  # reproducibilidad

# Ensayo clínico simulado:
# - 300 pacientes, 4 visitas (incluye basal y finales)
# - 3 brazos: Placebo, A, B
# - 10% de abandono (dropout) y 5% de missing en variables numéricas
#   para reflejar escenarios de práctica clínica y ensayos reales.
clinical_df <- clinical_data(
  n       = 300,
  visits  = 4,
  arms    = c("Placebo", "A", "B"),
  dropout = 0.10,
  missing = 0.05
)

# Aseguramos tipos adecuados
clinical_df <- clinical_df %>%
  mutate(
    treatment = factor(treatment),
    visit     = factor(visit, ordered = TRUE),
    response  = factor(response)
  )

str(clinical_df)
## 'data.frame':    1200 obs. of  8 variables:
##  $ participant_id: chr  "001" "001" "001" "001" ...
##  $ visit         : Ord.factor w/ 4 levels "1"<"2"<"3"<"4": 1 2 3 4 1 2 3 4 1 2 ...
##  $ sex           : Factor w/ 2 levels "Male","Female": 1 1 1 1 2 2 2 2 2 2 ...
##  $ treatment     : Factor w/ 3 levels "Placebo","A",..: 1 1 1 1 2 2 2 2 1 1 ...
##  $ age           : num  50 50 50 50 31 31 31 31 58 58 ...
##  $ weight        : num  61.9 66.7 63.3 62.1 87 85.3 NA NA 56.8 55.9 ...
##  $ biomarker     : num  43.5 53.6 52.7 54.9 41.4 ...
##  $ response      : Factor w/ 3 levels "Complete","Partial",..: NA 3 3 2 3 2 NA NA 3 3 ...
head(clinical_df)
##   participant_id visit    sex treatment age weight biomarker response
## 1            001     1   Male   Placebo  50   61.9     43.48     <NA>
## 2            001     2   Male   Placebo  50   66.7     53.59     None
## 3            001     3   Male   Placebo  50   63.3     52.69     None
## 4            001     4   Male   Placebo  50   62.1     54.92  Partial
## 5            002     1 Female         A  31   87.0     41.37     None
## 6            002     2 Female         A  31   85.3     51.59  Partial

1.1 Exploración Básica

La exploración inicial de la cohorte mediante una Tabla descriptiva, es un paso esencial para comprender la estructura del conjunto de datos y validar que las variables clave presentan comportamientos coherentes con el diseño del estudio. En este punto, el paquete {biostats} aporta un flujo estandarizado que facilita esta evaluación de forma reproducible y consistente.

summary_table() consolida estadísticas descriptivas relevantes (medias, medianas, IQR, proporciones, rangos) y reporta automáticamente el número total de observaciones y valores faltantes por variable. Esta información permite:

  • Evaluar la completitud de los datos, identificando variables con patrones relevantes de NA.

  • Describir la distribución interna de la cohorte, incluyendo composición por sexo, distribución de visitas, variabilidad del biomarcador y del peso corporal.

  • Revisar supuestos básicos (como normalidad), que son determinantes para la selección posterior de métodos paramétricos o no paramétricos.

  • Verificar consistencia del diseño, asegurando que las frecuencias por visita, tratamiento y respuesta sean plausibles.

Al centralizar esta información en una sola salida, {biostats} permite una lectura rápida y técnica del comportamiento basal del dataset, estableciendo una base sólida para los análisis inferenciales y longitudinales que se desarrollan en etapas posteriores.

1.2 Tabla descriptiva global

La tabla descriptiva global resume las características estructurales del conjunto de datos antes de cualquier análisis comparativo. Permite identificar la distribución de las variables demográficas y clínicas, evaluar la presencia de valores faltantes y revisar supuestos básicos como normalidad. Esta lectura inicial es clave para determinar qué modelos, pruebas y ajustes serán adecuados en el análisis posterior.

tabla_global <- summary_table(
data    = clinical_df,
all     = TRUE,
exclude = "participant_id"
)
tabla_global
variable n NAs summary normality
visit 1200 0 1: 300 (25.0%); 2: 300 (25.0%); 3: 300 (25.0%); 4: 300 (25.0%) NA
sex 1200 0 Male: 620 (51.7%); Female: 580 (48.3%) NA
treatment 1200 0 Placebo: 368 (30.7%); A: 424 (35.3%); B: 408 (34.0%) NA
age 1200 0 Mean (SD): 45.37 (14.4); Median (IQR): 44.00 (36.0,54.0); Range: 18.00,85.00 <0.001
weight 1200 106 Mean (SD): 70.28 (14.2); Median (IQR): 69.50 (60.5,79.6); Range: 45.00,118.80 <0.001
biomarker 1200 86 Mean (SD): 46.93 (10.1); Median (IQR): 47.22 (40.2,53.9); Range: 13.16,80.05 0.170
response 1200 167 Complete: 356 (34.5%); Partial: 208 (20.1%); None: 469 (45.4%) NA

El comportamiento observado corresponde a datos simulados bajo un diseño que replica la estructura típica de un ensayo clínico, por lo que la homogeneidad entre visitas y tratamientos es esperada y refleja un proceso de aleatorización controlado. La edad (age) y el peso (weigth) muestran distribuciones amplias y no normales (p < 0.001), mientras que el biomarcador presenta una distribución compatible con normalidad, coherente con los parámetros utilizados en la simulación.

Los porcentajes de datos faltantes en peso (8.8%) y respuesta (13.9%) emulan escenarios realistas de práctica clínica y deben considerarse posteriormente en análisis longitudinales o imputación. En conjunto, la tabla confirma que el dataset se comporta conforme a los supuestos del generador de datos de {biostats}, ofreciendo una base sólida y controlada para demostrar el flujo analítico.

1.3 Tabla comparativa Placebo vs A

La comparación entre dos grupos mediante summary_table() evalúa diferencias basales utilizando pruebas paramétricas o no paramétricas según la distribución observada. {biostats} selecciona automáticamente el test más adecuado e incorpora tamaños del efecto, facilitando una interpretación completa y reproducible de la magnitud y significancia de las diferencias entre grupos.

# Filtrar únicamente los grupos Placebo y A para el análisis comparativo
clinical_PA <- clinical_df %>%
filter(treatment %in% c("Placebo", "A"))

# Generar Tabla 1 comparativa entre Placebo y A:
# - Agrupa por tratamiento
# - Calcula estadísticas descriptivas
# - Selecciona automáticamente las pruebas de comparación
# - Incluye tamaños del efecto
# - Excluye participant_id por no ser informativa
tabla_PA <- summary_table(
data        = clinical_PA,
group_by    = "treatment",
all         = TRUE,
effect_size = TRUE,
exclude     = "participant_id"
)
tabla_PA
variable n NAs Placebo (Group A) A (Group B) normality test p_value effect_size effect_param
visit A:368, B:424 A: 0, B: 0 1: 92 (25.0%); 2: 92 (25.0%); 3: 92 (25.0%); 4: 92 (25.0%) 1: 106 (25.0%); 2: 106 (25.0%); 3: 106 (25.0%); 4: 106 (25.0%) NA Fisher 1.000 NA Cramer's V
sex A:368, B:424 A: 0, B: 0 Male: 168 (45.7%); Female: 200 (54.3%) Male: 224 (52.8%); Female: 200 (47.2%) NA Fisher 0.054 NA Cramer's V
age A:368, B:424 A: 0, B: 0 Mean (SD): 44.93 (14.6); Median (IQR): 45.00 (36.0,54.0); Range: 18.00,77.00 Mean (SD): 46.64 (14.5); Median (IQR): 45.00 (37.0,58.0); Range: 18.00,82.00 A:<0.001, B:<0.001 Mann-Whitney U 0.378 0.78 r
weight A:368, B:424 A: 25, B: 43 Mean (SD): 70.17 (15.5); Median (IQR): 69.60 (58.2,80.1); Range: 45.00,113.00 Mean (SD): 69.91 (13.9); Median (IQR): 68.80 (61.2,78.7); Range: 45.00,118.80 A:<0.001, B:<0.001 Mann-Whitney U 0.867 0.77 r
biomarker A:368, B:424 A: 22, B: 36 Mean (SD): 49.65 (10.2); Median (IQR): 50.46 (43.3,56.9); Range: 21.21,80.05 Mean (SD): 46.92 (9.3); Median (IQR): 47.26 (40.8,52.7); Range: 18.29,72.01 A:0.262, B:0.438 Welch's t-test <0.001 0.28 Cohen's d
response A:368, B:424 A: 58, B: 49 Complete: 49 (15.8%); Partial: 84 (27.1%); None: 177 (57.1%) Complete: 140 (37.3%); Partial: 73 (19.5%); None: 162 (43.2%) NA Fisher <0.001 NA Cramer's V

En este dataset simulado, los grupos Placebo y A fueron generados sin diferencias sistemáticas en las variables continuas ni en la estructura demográfica, lo cual se refleja en p-valores no significativos y tamaños del efecto cercanos a cero. La ligera variación en la proporción de sexo (p = 0.054) es consistente con la aleatorización simulada y no representa un desequilibrio relevante. La variable de respuesta muestra diferencias significativas (p < 0.001), atribuibles al mecanismo de simulación que asigna categorías de respuesta condicionadas al biomarcador y no necesariamente a efectos reales del tratamiento. En síntesis, la tabla confirma que el generador de datos de {biostats} produce grupos comparables y adecuados para demostrar análisis comparativos estandarizados, sin inducir sesgos estructurales no deseados.

2 Valores perdidos y atípicos

La identificación de valores faltantes y atípicos es crítica para evitar sesgos y elegir estrategias de imputación o manejo de datos extremos.

2.1 Valores perdidos (missing_values())

La función missing_values() de {biostats} resume de forma integrada el volumen y la distribución de datos faltantes, tanto a nivel de filas como de variables, e incorpora un panel gráfico con proporción de NA por variable y patrón de ausencia. Esta salida permite dimensionar el impacto potencial del missingness y decidir si basta con un análisis completo-casos o si se requieren estrategias de imputación o análisis de sensibilidad.

mv <- missing_values(
data = clinical_df,
all  = TRUE
)
mv
## 
## Missing Value Analysis
## 
## Complete rows: 975 (81.2%)
## Missing cells: 359 (3.7%)
## 
##                n_missing pct_missing
## response             167       13.92
## weight               106        8.83
## biomarker             86        7.17
## participant_id         0        0.00
## visit                  0        0.00
## sex                    0        0.00
## treatment              0        0.00
## age                    0        0.00

En este conjunto simulado, el 81.2% de las filas están completas (975/1200) y solo el 3.7% de las celdas presentan valores faltantes, lo que indica una pérdida global baja. Sin embargo, la ausencia no es homogénea: se concentra en response (13.9%), weight (8.8%) y biomarker (7.2%), mientras que las variables de identificación, visita, sexo, tratamiento y edad no tienen NA. El panel de patrones muestra que los faltantes se distribuyen a lo largo del seguimiento, sin un bloqueo evidente en una visita específica, lo que sugiere un mecanismo más cercano a MAR que a un fallo sistemático del diseño. En un análisis aplicado, estas magnitudes justificarían al menos comparar resultados entre análisis de casos completos e imputación múltiple para las variables clínicas clave.

2.2 Outliers en el biomarcador (outliers())

La función outliers() implementa la detección univariada de valores atípicos mediante el criterio de Tukey (IQR × 1.5), informando el número de observaciones extremas, los límites inferior y superior utilizados y los índices implicados. Además, genera un gráfico combinado de dispersión por índice y boxplot, que facilita juzgar si esos puntos reflejan errores de registro, variación biológica extrema o patrones que ameritan modelos más robustos.

out_biomarker <- outliers(
data = clinical_df,
x    = "biomarker"
)
out_biomarker
## 
## Outlier Analysis
## 
## Variable: 'biomarker'
## n: 1114
## Missing: 86 (7.2%)
## Method: Tukey's IQR x 1.5
## Bounds: [19.636, 74.506]
## Outliers detected: 7 (0.6%)
## 
## Outlier indices: 35, 102, 327, 420, 425, 445, 730

Para el biomarcador, se analizan 1114 observaciones no faltantes, con 86 NA ya contabilizados previamente. Con los límites [19.64, 74.51] definidos por el método de Tukey, se identifican solo 7 valores fuera de rango (0.6%), lo que indica una distribución muy concentrada y con baja proporción de extremos. Los puntos resaltados en el gráfico se ubican en los bordes de la distribución esperada para este escenario simulado y no configuran un problema de outliers masivo. En un contexto aplicado, estos valores podrían mantenerse en el análisis —eventualmente acompañado de métodos robustos o transformaciones—, documentando su presencia más como variación natural que como indicio de errores sistemáticos en la captura de datos.

3 Evaluación de normalidad (normality())

La evaluación de normalidad en variables continuas es un paso clave para decidir entre pruebas paramétricas o no paramétricas. La función normality() de {biostats} combina pruebas formales (Kolmogorov–Smirnov, Shapiro–Wilk) con medidas de simetría y curtosis, además de generar Q–Q plots e histogramas con densidad ajustada. Esta integración permite juzgar de forma consistente si los datos se aproximan a una distribución normal en cada grupo.

baseline <- clinical_df %>%
filter(visit == levels(visit)[1])

for (g in levels(baseline$treatment)) {
cat("\n--- Normalidad en:", g, "---\n")
print(
normality(
data = baseline %>% filter(treatment == g),
x    = "biomarker"
)
)
}
## 
## --- Normalidad en: Placebo ---
## 
## Normality Test for 'biomarker' 
## 
## n = 91 
## mean (SD) = 50.36 (10.7) 
## median (IQR) = 51.51 (13.8) 
## 
## Kolmogorov-Smirnov: D = 0.066, p = 0.795 
## Shapiro-Wilk: W = 0.982, p = 0.242 
## Skewness: -0.36 (z = -1.41) 
## Kurtosis: 0.47 (z = 0.95) 
## 
## Data appears normally distributed.
##  
## (Use all = TRUE to see values outside 95%CI [1]).

## 
## --- Normalidad en: A ---
## 
## Normality Test for 'biomarker' 
## 
## n = 104 
## mean (SD) = 47.72 (9.6) 
## median (IQR) = 46.89 (14.2) 
## 
## Kolmogorov-Smirnov: D = 0.064, p = 0.787 
## Shapiro-Wilk: W = 0.987, p = 0.441 
## Skewness: 0.26 (z = 1.09) 
## Kurtosis: -0.22 (z = -0.47) 
## 
## Data appears normally distributed.
## 

## 
## --- Normalidad en: B ---
## 
## Normality Test for 'biomarker' 
## 
## n = 101 
## mean (SD) = 44.32 (10.1) 
## median (IQR) = 44.60 (15.3) 
## 
## Kolmogorov-Smirnov: D = 0.068, p = 0.738 
## Shapiro-Wilk: W = 0.984, p = 0.284 
## Skewness: -0.30 (z = -1.25) 
## Kurtosis: -0.07 (z = -0.15) 
## 
## Data appears normally distributed.
## 

En la visita basal, el biomarcador muestra una distribución compatible con normalidad en los tres brazos del estudio (Placebo, A y B). Tanto Shapiro–Wilk como Kolmogorov–Smirnov presentan valores p no significativos, y las métricas de asimetría y curtosis se encuentran dentro de rangos aceptables. Los Q–Q plots muestran alineación con la recta teórica, con pocos puntos fuera del intervalo del 95%.

Dado que los datos son simulados bajo parámetros controlados, este comportamiento homogéneo era esperable y confirma que las diferencias observadas entre grupos no están afectadas por distorsiones severas en la forma de la distribución. Esta evidencia respalda el uso de métodos paramétricos en comparaciones posteriores del biomarcador, siempre que se cumplan los demás supuestos del modelo.

4 Visualización descriptiva

La visualización exploratoria es un componente esencial para obtener una lectura inmediata del comportamiento de las variables clave y validar supuestos antes de aplicar modelos inferenciales. El paquete {biostats} integra funciones estandarizadas para la generación de gráficos diagnósticos que permiten identificar patrones de distribución, relaciones entre variables y diferencias entre grupos de tratamiento. Esto resulta especialmente útil en datasets simulados, donde los patrones esperados deben corresponder con el diseño del escenario generador de datos.

4.1 Histograma global del biomarcador

El histograma se construye con plot_hist(), una función de {biostats} que automatiza la inspección de distribuciones univariadas incorporando, en una sola llamada, una estética estandarizada, escalas consistentes y densidad ajustada sin necesidad de código adicional. A diferencia del flujo habitual con ggplot2, donde cada elemento debe especificarse manualmente, {biostats} prioriza rapidez, reproducibilidad y uniformidad visual en análisis clínicos o epidemiológicos.

plot_hist(
data  = clinical_df,
x     = "biomarker",
title = "Distribución del biomarcador (todas las visitas)"
)

En nuestro resultado, el biomarcador muestra una distribución aparentemente simétrica, centrada entre 45–55 unidades, sin colas pronunciadas ni multimodalidad. Este patrón es coherente con los parámetros de simulación y sugiere que el biomarcador mantiene una variabilidad estable en toda la cohorte. La ausencia de valores extremos relevantes respalda el uso de métodos paramétricos en análisis posteriores, siempre que se confirme esta estabilidad dentro de subgrupos.

4.2 Proporción de respuesta por tratamiento

El gráfico proviene de plot_bar(), función que simplifica la generación de barras apiladas y proporcionales integrando automáticamente cálculo de proporciones, anotaciones y formateo clínico. A diferencia de construir manualmente capas en ggplot2 o transformar previamente tablas, {biostats} crea una representación lista para interpretación, especialmente útil en análisis por tratamiento o comparaciones descriptivas entre grupos.

plot_bar(
data     = clinical_df,
x        = "treatment",
group    = "response",
position = "fill",
title    = "Proporción de categorías de respuesta por tratamiento",
values   = TRUE
)

La distribución de respuesta varía entre tratamientos: el grupo B presenta mayor proporción de respuesta completa, mientras que Placebo muestra predominio de ausencia de respuesta. Este patrón es consistente con el comportamiento esperado en escenarios simulados donde los brazos activos suelen mostrar mayor eficacia. Aunque no implica significancia por sí mismo, orienta el análisis comparativo formal y señala dónde evaluar efectos diferenciales de tratamiento.

4.3 Matriz de correlación (edad, peso, biomarcador)

La visualización se genera con plot_corr(), que combina en una sola función el cálculo de correlaciones, su significancia y la representación gráfica en formato estandarizado. Mientras que en otros paquetes es necesario usar cor(), cor.test() y luego construir un gráfico manual, {biostats} entrega un panel completo, ordenado y clínicamente interpretable, ideal para exploración rápida de dependencias entre variables continuas.

corr_data <- clinical_df %>%
select(age, weight, biomarker)

plot_corr(
data      = corr_data,
vars      = c("age", "weight", "biomarker"),
type      = "lower",
show_sig  = TRUE,
sig_only  = FALSE,
title     = "Matriz de correlación (edad, peso, biomarcador)"
)

Las correlaciones entre edad, peso y biomarcador son bajas, sin asociaciones lineales relevantes. Esto indica independencia práctica entre las variables y sugiere que el biomarcador no depende de características demográficas o antropométricas básicas en este escenario simulado. La ausencia de correlación significativa también reduce preocupación por multicolinealidad en modelos posteriores y confirma la consistencia del proceso de simulación.

5 Comparación entre tratamientos en la visita final (omnibus())

La función omnibus() de {biostats} ofrece un enfoque automatizado para comparar grupos cuando el investigador no quiere decidir manualmente entre ANOVA o Kruskal–Wallis. A diferencia de otros paquetes que requieren especificar explícitamente el test, {biostats} evalúa supuestos, selecciona el método adecuado y entrega una salida estandarizada con estadísticos, valor p y conclusiones. Esto reduce errores analíticos y mantiene un flujo coherente incluso en datasets simulados como este.

final <- clinical_df %>%
filter(visit == last(levels(visit)))

omnibus_trt <- omnibus(
data = final,
y    = "biomarker",
x    = "treatment"
)
omnibus_trt
## 
## Omnibus Test: Kruskal-Wallis
## 
## Assumption Testing Results:
## 
##   Normality (Shapiro-Wilk Test):
##   Placebo: W = 0.9851, p = 0.465
##   A: W = 0.9678, p = 0.024
##   B: W = 0.9737, p = 0.065
##   Overall result: Non-normal distribution detected.
## 
##   Homogeneity of Variance (Levene Test):
##   F(2,260) = 0.4210, p = 0.657
##   Effect size (eta-squared) = 0.0032
##   Result: Homogeneous variances.
## 
## Test Results:
##   Formula: biomarker ~ treatment
##   alpha: 0.05
##   Result: significant (p = <0.001)
## 
## Post-hoc Multiple Comparisons
## 
##   Pairwise Wilcoxon-tests (alpha: 0.050) (p_method: holm):
##                    Placebo           A
##   A                   0.245             -
##   B                   0.001*        0.016*
## 
## The study groups show a moderately imbalanced distribution of sample sizes (Δn = 0.199).

El análisis global entre Placebo, A y B en la visita final muestra diferencias estadísticamente significativas en el biomarcador, con un p < 0.001 según el test de Kruskal–Wallis, que {biostats} seleccionó automáticamente tras identificar no normalidad en al menos un brazo (Shapiro–Wilk p = 0.024 en tratamiento A). A pesar de ello, las varianzas resultaron homogéneas, lo que refuerza la consistencia del contraste.

Los análisis post-hoc (Wilcoxon pareado con corrección Holm) indican que:

  • Placebo vs A: no presenta diferencias significativas (p = 0.245).

  • Placebo vs B: diferencia significativa (p = 0.001).

  • A vs B: diferencia significativa (p = 0.016).

Este patrón muestra que el brazo B concentra los valores más bajos del biomarcador, mientras que Placebo y A mantienen distribuciones más próximas entre sí. La función también reporta una leve desproporción en el tamaño muestral (Δn ≈ 0.20), aunque insuficiente para comprometer el resultado.

Dado que los datos proceden de un generador simulado con diferencias programadas entre brazos, la detección de significancia en los contrastes con B es consistente con el diseño del dataset. No obstante, su interpretación se restringe a fines ilustrativos, pues no representa un efecto real sino un escenario construido para demostración.

5.1 Boxplot en visita final

La función plot_box() de {biostats} construye boxplots estandarizados con soporte para agrupamiento, puntos individuales, líneas de resumen y colores consistentes con los demás gráficos del paquete. Su ventaja frente a alternativas de {ggplot2} radica en que no requiere código adicional para mostrar outliers, medianas o estadísticas clave, facilitando la visualización comparativa entre tratamientos.

plot_box(
data   = final,
x      = "treatment",
y      = "biomarker",
group  = "treatment",
title  = "Biomarcador por tratamiento – visita final",
xlab   = "Tratamiento",
ylab   = "Biomarcador",
points = TRUE
)

El boxplot revela un gradiente descendente claro en los valores del biomarcador desde Placebo hacia el brazo B. Las medianas de cada grupo muestran diferencias visibles: Placebo y A presentan niveles similares, mientras que B exhibe un desplazamiento consistente hacia valores menores, concordante con los resultados post-hoc que identificaron diferencias significativas únicamente en los contrastes que involucran a B.

La dispersión dentro de cada brazo es comparable, sin indicios de heterocedasticidad, lo cual coincide con el test de Levene que confirmó varianzas homogéneas. Los valores atípicos son escasos y no modifican la interpretación global del patrón.

Este comportamiento reproduce fielmente la estructura impuesta en los datos simulados, donde el brazo B fue diseñado con una reducción sistemática del biomarcador. En ese sentido, el gráfico ofrece una representación visual coherente con el hallazgo del análisis no paramétrico, reforzando que la diferencia relevante se concentra en la separación entre B y los otros dos tratamientos.

6 Análisis longitudinal (medidas repetidas)

omnibus() también permite realizar contrastes longitudinales en diseños con medidas repetidas. {biostats} se encarga de verificar que exista una observación por sujeto-visita y selecciona automáticamente entre ANOVA-RM o Friedman según normalidad y estructura de varianzas. Esto evita la especificación manual de modelos y facilita un análisis rápido de tendencias temporales.

n_visitas <- clinical_df %>%
filter(!is.na(biomarker)) %>%
pull(visit) %>%
n_distinct()

clinical_long_clean <- clinical_df %>%
filter(!is.na(biomarker)) %>%
group_by(participant_id, visit) %>%
slice(1) %>%
ungroup()

clinical_complete <- clinical_long_clean %>%
group_by(participant_id) %>%
filter(n_distinct(visit) == n_visitas) %>%
ungroup()

omnibus_long <- omnibus(
data      = clinical_complete,
y         = "biomarker",
x         = "visit",
paired_by = "participant_id",
alpha     = 0.05
)
omnibus_long
## 
## Omnibus Test: Friedman
## 
## Assumption Testing Results:
## 
##   Sphericity (Mauchly Test):
##   W = 0.9867, p = 0.653
##   Result: Sphericity assumed.
## 
##   Normality (Shapiro-Wilk Test):
##   1: W = 0.9953, p = 0.644
##   2: W = 0.9975, p = 0.970
##   3: W = 0.9956, p = 0.706
##   4: W = 0.9868, p = 0.021
##   Overall result: Non-normal distribution detected.
## 
##   Homogeneity of Variance (Levene Test):
##   F(3,992) = 1.0138, p = 0.386
##   Effect size (eta-squared) = 0.0031
##   Result: Homogeneous variances.
## 
## Test Results:
##   Formula: biomarker ~ visit | participant_id
##   alpha: 0.05
##   Result: not significant (p = 0.774)
## Post-hoc tests not performed (results not significant).
## 
## The study groups show a moderately imbalanced distribution of sample sizes (Δn = 0.214).

El análisis longitudinal mediante el test de Friedman no identifica diferencias significativas en la evolución del biomarcador a lo largo de las cuatro visitas (p = 0.774). A pesar de que una de las visitas muestra desviación de normalidad, la función confirma esfericidad adecuada (Mauchly p = 0.653) y varianzas homogéneas, lo cual valida el uso de un enfoque no paramétrico para medidas repetidas.

El resultado no significativo indica que, en este conjunto simulado, el biomarcador no cambia de forma sistemática dentro de los sujetos a través del tiempo, y cualquier variación observada entre visitas es atribuible al azar más que a un efecto temporal consistente.

Este hallazgo contrasta con el patrón esperado en algunos diseños clínicos reales, pero es completamente coherente con el generador de datos utilizado, que en esta simulación particular no introdujo un gradiente temporal definido dentro del mismo individuo. Por ello, no se realizan pruebas post-hoc.

En resumen, el análisis longitudinal confirma estabilidad intra-sujeto del biomarcador a través de las visitas, reflejando la estructura neutral del dataset simulado para este componente.

6.1 Curva de evolución por tratamiento

plot_line() es una función de {biostats} diseñada para visualización de tendencias, incorporando automáticamente medias por punto temporal y barras de error (SE o SD). A diferencia de gráficas manuales en {ggplot2}, este enfoque garantiza consistencia estética y cálculo correcto de estadísticas de resumen.

plot_line(
data        = clinical_df,
x           = "visit",
y           = "biomarker",
group       = "treatment",
stat        = "mean",
error       = "se",
title       = "Evolución promedio del biomarcador por tratamiento",
xlab        = "Visita",
ylab        = "Biomarcador",
legend_title = "Tratamiento"
)

La evolución promedio del biomarcador muestra fluctuaciones leves entre visitas, pero sin un patrón ascendente o descendente consistente dentro de cada brazo. Placebo, A y B mantienen trayectorias relativamente paralelas, con variaciones pequeñas en torno a sus niveles medios característicos de cada tratamiento.

Este comportamiento concuerda con el resultado del test de Friedman (p = 0.774), que indica ausencia de cambios significativos a lo largo del tiempo dentro de los sujetos. Las diferencias observadas entre puntos corresponden a variabilidad aleatoria, no a un efecto temporal sistemático.

Las barras de error estrechas reflejan una variabilidad intra-visita baja, propia del dataset simulado, pero dicha estabilidad no se traduce en una tendencia longitudinal significativa. En consecuencia, la gráfica confirma visualmente que las diferencias entre tratamientos se mantienen estables en el tiempo, mientras que la evolución temporal del biomarcador es esencialmente plana dentro de cada grupo.

7 Medidas de efecto epidemiológico (effect_measures())

La función effect_measures() de {biostats} automatiza el cálculo de OR, RR, RD y NNT a partir de una tabla 2×2. Integra correcciones por celdas vacías, intervalos de confianza y valores p en una salida única, algo que habitualmente requiere múltiples funciones dispersas en otros paquetes.

cutoff <- quantile(
final$biomarker[final$treatment == "Placebo"],
probs = 0.25,
na.rm = TRUE
)

final_PA <- final %>%
filter(treatment %in% c("Placebo", "A")) %>%
mutate(
exposed = ifelse(treatment == "A", 1, 0),
event   = ifelse(biomarker <= cutoff, 1, 0)
)

tab_2x2 <- final_PA %>%
count(exposed, event) %>%
pivot_wider(
names_from  = event,
values_from = n,
values_fill = 0
)
tab_2x2
## # A tibble: 2 × 4
##   exposed   `0`   `1`  `NA`
##     <dbl> <int> <int> <int>
## 1       0    61    21    10
## 2       1    63    28    15
exposed_event      <- tab_2x2$`1`[tab_2x2$exposed == 1]
exposed_no_event   <- tab_2x2$`0`[tab_2x2$exposed == 1]
unexposed_event    <- tab_2x2$`1`[tab_2x2$exposed == 0]
unexposed_no_event <- tab_2x2$`0`[tab_2x2$exposed == 0]

efectos <- effect_measures(
exposed_event      = exposed_event,
exposed_no_event   = exposed_no_event,
unexposed_event    = unexposed_event,
unexposed_no_event = unexposed_no_event,
alpha              = 0.05,
correction         = TRUE
)
efectos
## 
## Odds/Risk Ratio Analysis
## 
## Contingency Table:
##                 Event No Event      Sum
## Exposed            28       63       91
## Unexposed          21       61       82
## Sum                49      124      173
## 
## Odds Ratio: 1.291 (95% CI: 0.663 - 2.514)
## Risk Ratio: 1.201 (95% CI: 0.743 - 1.943)
## 
## Risk in exposed: 30.8%
## Risk in unexposed: 25.6%
## Absolute risk difference: 5.2%
## Number needed to harm (NNH): 19.4
## 
## Note: Correction not applied (no zero values).

El punto de corte basado en el percentil 25 del biomarcador en el brazo Placebo permite definir un desenlace de “buen control” coherente con la estructura del dataset. Al comparar Placebo con el tratamiento A, la tabla 2×2 resultante muestra proporciones similares de eventos entre ambos grupos (30.8% en A vs. 25.6% en Placebo), lo que se refleja en estimadores de efecto cercanos a la unidad.

El Odds Ratio (1.29; IC95%: 0.66–2.51) y el Risk Ratio (1.20; IC95%: 0.74–1.94) indican una tendencia leve hacia mayor probabilidad de buen control en el tratamiento A, aunque con intervalos amplios que cruzan el valor nulo, lo que implica ausencia de significancia estadística. La diferencia absoluta de riesgo (5.2%) es pequeña y el NNH de 19.4 enfatiza que estas diferencias pueden atribuirse al azar dentro del contexto simulado.

Este patrón es congruente con el diseño del dataset, en el cual las diferencias entre A y Placebo no fueron parametrizadas para ser marcadamente divergentes en la distribución final del biomarcador. Así, {biostats} facilita una lectura completa del efecto mediante una salida integrada que resume riesgo, odds, diferencias absolutas e intervalos de confianza en un único paso.

8 Visualizaciones adicionales

Las funciones visuales de {biostats} (como plot_hist() y plot_box()) permiten extender las exploraciones hacia modelos estratificados por tratamiento o por visita sin necesidad de reconfigurar temas, escalas o códigos adicionales. Esto hace que las visualizaciones mantengan uniformidad y reduzcan la carga de maquetación, como ya se ha mencionado, a continuación se exponen ejemplos de otras visualizaciones que se consideran relevantes.

plot_hist(
data   = clinical_df,
x      = "biomarker",
facet  = "treatment",
title  = "Distribución del biomarcador por tratamiento"
)

El histograma estratificado revela tres distribuciones claramente diferenciadas, con desplazamientos en la media que reproducen fielmente los parámetros del generador simulado. Placebo se centra alrededor de valores más altos, seguido por A y luego B, manteniendo varianzas relativamente comparables entre grupos.

Esta separación visual confirma la estructura experimental programada: cada brazo fue diseñado con un nivel basal distinto del biomarcador para ilustrar contrastes clínicos plausibles. La forma ligeramente simétrica de las distribuciones, junto con la ausencia de colas extremas, es coherente con la normalidad observada en los análisis previos.

A diferencia de funciones base de R o de ggplot2 puras, {biostats} automatiza la estratificación y aplica escalas y estilos homogéneos entre facetas, permitiendo verificar en segundos si la simulación o el diseño real siguen un patrón esperado entre brazos.

plot_box(
data   = clinical_df,
x      = "visit",
y      = "biomarker",
group  = "treatment",
title  = "Biomarcador por visita y tratamiento",
xlab   = "Visita",
ylab   = "Biomarcador"
)

Los boxplots estratificados por visita y tratamiento confirman un patrón temporal estable:

  • Placebo mantiene valores consistentemente superiores.

  • A ocupa una posición intermedia.

  • B refleja la reducción más marcada del biomarcador, respetando la jerarquía definida en la simulación.

Aunque los descensos entre visitas no alcanzan significancia en el análisis longitudinal (Friedman p = 0.774), los gráficos muestran fluctuaciones menores compatibles con la variabilidad aleatoria incorporada en el simulador. Se observan pocos atípicos y una dispersión estable entre brazos y momentos, reforzando el control de varianza impuesto en el modelo generador.

Esta visualización permite validar rápidamente si existe coherencia entre el comportamiento longitudinal observado y los parámetros simulados. {biostats} aporta un plus al producir una representación estándar, con medianas, medias y distribución completa por visita y brazo sin necesidad de codificación manual adicional.

9 Cálculo de tamaño de muestra (sample_size() y sample_size_range())

Finalmente se ilustran las funciones sample_size() y sample_size_range() de {biostats} las cuales encapsulan fórmulas estándar de cálculo de potencia para ensayos clínicos, eliminando la necesidad de codificar manualmente ecuaciones para superioridad, no inferioridad o diferencias esperadas. El usuario define parámetros clínicos clave y la función devuelve n ajustada por dropout.

ss_sup <- sample_size(
  sample  = "two-sample",
  design  = "parallel",
  outcome = "mean",
  type    = "superiority",
  alpha   = 0.05,
  beta    = 0.20,
  x1      = 75,   # tratamiento
  x2      = 70,   # control
  SD      = 10,
  delta   = 3,    # Δ < (x1 - x2) = 5
  dropout = 0.10,
  k       = 1
)

ss_sup
## 
## Sample Size Calculation
## 
## Test type: superiority
## Design: parallel, two-sample
## Outcome: mean
## Alpha (α): 0.050
## Beta (β): 0.200
## Power: 80.0%
## 
## Parameters:
## x1 (treatment): 75.000
## x2 (control/reference): 70.000
## Difference (x1 - x2): 5.000
## Standard Deviation (σ): 10.000
## Allocation Ratio (k): 1.00
## Delta (δ): 3.000
## Dropout rate: 10.0%
## 
## Required Sample Size
## n1 = 341
## n2 = 341
## Total = 682
## 
## Note: Sample size increased by 10.0% to account for potential dropouts.

Ensayo de superioridad. El escenario de superioridad, basado en una diferencia esperada de 5 unidades con desviación estándar de 10, arroja un tamaño por grupo de 341 participantes, para un total de 682, ya ajustados por un 10% de pérdidas.

Este resultado es consistente con situaciones reales donde el efecto esperado es moderado y la variabilidad clínica relevante. La magnitud del tamaño muestral refleja adecuadamente el equilibrio entre potencia (80%) y la necesidad de detectar una diferencia clínicamente significativa.El escenario de superioridad, basado en una diferencia esperada de 5 unidades con desviación estándar de 10, arroja un tamaño por grupo de 341 participantes, para un total de 682, ya ajustados por un 10% de pérdidas.

Este resultado es consistente con situaciones reales donde el efecto esperado es moderado y la variabilidad clínica relevante. La magnitud del tamaño muestral refleja adecuadamente el equilibrio entre potencia (80%) y la necesidad de detectar una diferencia clínicamente significativa.

ss_ni <- sample_size(
sample  = "two-sample",
design  = "parallel",
outcome = "mean",
type    = "non-inferiority",
alpha   = 0.025,
beta    = 0.20,
x1      = 70,
x2      = 70,
SD      = 10,
delta   = -5,
dropout = 0.10,
k       = 1
)
ss_ni
## 
## Sample Size Calculation
## 
## Test type: non-inferiority
## Design: parallel, two-sample
## Outcome: mean
## Alpha (α): 0.025
## Beta (β): 0.200
## Power: 80.0%
## 
## Parameters:
## x1 (treatment): 70.000
## x2 (control/reference): 70.000
## Difference (x1 - x2): 0.000
## Standard Deviation (σ): 10.000
## Allocation Ratio (k): 1.00
## Delta (δ): -5.000
## Dropout rate: 10.0%
## 
## Required Sample Size
## n1 = 70
## n2 = 70
## Total = 140
## 
## Note: Sample size increased by 10.0% to account for potential dropouts.

Ensayo de no inferioridad. El contraste de no inferioridad, con delta = –5, muestra un requerimiento notablemente menor: 70 participantes por grupo (total 140).

Este comportamiento es metodológicamente esperado:

  • Cuando la diferencia verdadera entre grupos es cero (x1 = x2),

  • Y el margen de no inferioridad es amplio (–5). La potencia requerida se alcanza con tamaños sustancialmente más pequeños.

Este ejemplo evidencia cómo {biostats} facilita la comparación entre diseños alternativos bajo los mismos supuestos de variabilidad, ayudando a dimensionar el impacto operativo de la elección del margen Δ.

ss_range <- sample_size_range(
x1_range = c(65, 75),
x2       = 70,
step     = 1,
sample   = "two-sample",
design   = "parallel",
outcome  = "mean",
type     = "non-inferiority",
SD       = 10,
delta    = -5,
dropout  = 0.10
)
ss_range
## 
## Sample Size Range Analysis
## 
## Treatment range (x1): 65.000 to 66.000
## Control/Reference (x2): 70.000
## Step size: 1.000
## 
## 70% Power: Total n = 22 to 2074
## 80% Power: Total n = 30 to 2722
## 90% Power: Total n = 40 to 3770
## 
## Sample size increased by 10.0% to account for potential dropouts.

Análisis de rango (sensibilidad). El análisis de sensibilidad muestra de forma visual cómo pequeñas variaciones en la media esperada del tratamiento modifican drásticamente el tamaño de muestra necesario.

Resultados clave:

  • Para medias del tratamiento apenas inferiores al control (65–66), los tamaños totales pueden sobrepasar los 3.000 participantes.

  • A medida que x1 se aproxima a x2, la curva se estabiliza rápidamente en tamaños reducidos.

  • Las tres curvas de potencia (70%, 80%, 90%) conservan la misma forma, divergiendo únicamente en el umbral de muestra requerido.

Este comportamiento es plenamente coherente con la teoría: cuanto menor es el efecto esperado, mayor n se necesita para detectarlo. La función sample_size_range() automatiza este análisis, produciendo una visualización clara y directamente utilizable en informes de diseño o discusiones metodológicas.

Los tres resultados confirman que {biostats} reproduce correctamente los cálculos fundamentales para planificación de ensayos clínicos, permitiendo:

  • Estimar tamaños por diseño (superioridad vs. no inferioridad).

  • Ajustar por pérdidas sin cálculos manuales.

  • Explorar rangos de escenarios de manera visual e interpretable.

Aunque los parámetros provienen de un ejercicio teórico y no de un protocolo real, las funciones operan bajo estándares epidemiológicos y estadísticos formalmente aceptados, ofreciendo una plataforma fiable para docencia, simulación o estructuración preliminar de estudios clínicos.

Este conjunto de procedimientos constituye una ruta metodológica reproducible para la planificación estadística en ensayos clínicos, integrando pruebas globales, análisis longitudinales, medidas de efecto y cálculos de tamaño de muestra en un flujo unificado mediante {biostats}.
La estandarización de funciones y la capacidad de ajustar supuestos permiten replicar escenarios reales y adaptar los análisis a diferentes diseños, niveles de evidencia y requerimientos operativos.

🔗 LinkedIn: https://www.linkedin.com/in/camilogz