A. Carga de librerías.

library(openxlsx)
## Warning: package 'openxlsx' was built under R version 4.4.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.4.3
## 
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(gtsummary)
## Warning: package 'gtsummary' was built under R version 4.4.3
library(gt)
## Warning: package 'gt' was built under R version 4.4.3
library(blockrand)
library(tidyverse) 
## Warning: package 'tidyverse' was built under R version 4.4.3
## Warning: Can't find generic `as.gtable` in package gtable to register S3 method.
## ℹ This message is only shown to developers using devtools.
## ℹ Do you need to update gtable to the latest version?
## Warning: package 'tidyr' was built under R version 4.4.3
## Warning: package 'forcats' was built under R version 4.4.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.5
## ✔ ggplot2   3.5.1     ✔ stringr   1.5.1
## ✔ lubridate 1.9.3     ✔ tibble    3.2.1
## ✔ purrr     1.0.2     ✔ tidyr     1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(gtsummary)
library(openxlsx)

B. Carga de datos.

data <- read.xlsx("C:/Users/Usuario/Desktop/iecs/ano_2/ensayos_clínicos/encuentro_sincrónico_del_14_06/Data_TCC.xlsx")

C. Preparación de datos.

Convertir Group a factor con etiquetas.

data$Group <- factor(data$Group, levels = c("CCT", "Hands-off"),
                     labels = c("CCT group", "Hands-off group"))

Convertir Nulliparous a factor.

data$Nulliparous <- factor(data$Nulliparous, levels = c(1, 0),
                           labels = c("Yes", "No"))

Convertir Gestational_age a numérica.

data$Gestational_age <- as.numeric(data$Gestational_age)

D. Ejercicio 1.

Replicar la Tabla 1 del Paper con las variables disponibles en el data set (Maternal_age, Gestational_age,Birth_weight, Nulliparous, Group) con tbl_summary.

# Tabla resumen final
tabla1 <- data %>%
  select(Group, Nulliparous, Gestational_age, Maternal_age, Birth_weight) %>%
  tbl_summary(
    by = Group,
    type = list(c(Gestational_age, Maternal_age) ~ "continuous"),
    statistic = list(
      all_continuous() ~ "{mean} ± {sd}",
      all_categorical() ~ "{n} ({p}%)"
    ),
    digits = all_continuous() ~ 1,
    missing = "no"
  ) %>%
  add_n() %>%
  modify_header(label = "**Characteristic**") %>%
  modify_caption("**Tabla 1. Características basales por grupo.**")

tabla1
Tabla 1. Características basales por grupo.
Characteristic N CCT group
N = 103
1
Hands-off group
N = 101
1
Nulliparous 199 37 (37%) 36 (36%)
Gestational_age 204 39.1 ± 1.5 39.0 ± 1.2
Maternal_age 204 25.8 ± 6.1 25.3 ± 6.4
Birth_weight 199 3,293.7 ± 541.2 3,262.1 ± 539.5
1 n (%); Mean ± SD

No hay diferencias llamativas ni clínicamente significativas entre los grupos, lo que sugiere que la aleatorización fue exitosa y los grupos son comparables al inicio del estudio. Esto es esencial para garantizar que cualquier diferencia en los desenlaces se deba al efecto del tratamiento y no a diferencias basales.

E. Ejercicio 2.

Obtener la distribución de mujeres aleatorizadas a cada grupo según hospital ¿Se mantiene el balance de grupos para cada hospital?

tabla2 <- data %>%
  select(Hospital, Group) %>%
  tbl_summary(
    by = Hospital,
    statistic = list(all_categorical() ~ "{n} ({p}%)"),
    digits = all_categorical() ~ 1,
    missing = "no"
  ) %>%
  bold_labels()

as_gt(tabla2)
Characteristic Hospital A
N = 70
1
Hospital B
N = 134
1
Group

    CCT group 35.0 (50.0%) 68.0 (50.7%)
    Hands-off group 35.0 (50.0%) 66.0 (49.3%)
1 n (%)

🔎 Implicancias metodológicas y clínicas:

  1. Desbalance en el aporte de pacientes por centro

Aunque la aleatorización mantuvo un buen balance entre los grupos dentro de cada hospital, el número total de pacientes reclutados por cada hospital no es equitativo. Esto puede tener varias implicancias:

  1. Implicancia en la representatividad

Si los hospitales tienen características estructurales distintas, por ejemplo, diferentes protocolos de atención, experiencia del equipo médico, población atendida, etc., entonces la mayor representación del Hospital B podría sesgar los resultados.

Ejemplo: si Hospital B atiende mayor proporción de embarazos de alto riesgo, eso podría afectar el desenlace principal si no se ajusta por hospital en el análisis.

  1. Potencial confusión o efecto de centro

El hospital puede actuar como un confusor o modificador de efecto. Esto se puede manejar:

  • Ajustando por hospital en los modelos multivariables.
  • Incluyéndolo como efecto aleatorio si se realiza un modelo mixto.
  • Estratificando el análisis por centro si se sospecha interacción.
  1. Generalización de resultados
  • El análisis puede estar más influenciado por los datos del Hospital B.
  • Esto puede limitar la extrapolación si los hospitales tienen prácticas clínicas distintas o atienden poblaciones diferentes.
  1. Aleatorización estratificada ≠ igual tamaño por estrato
  • Aunque se usó aleatorización estratificada, no se forzó el tamaño de muestra igual por hospital.

  • Esto es común cuando se usa aleatorización por bloques estratificada, pero no se fija un tamaño de reclutamiento igual por centro.

F. Ejercicio 3.

Realizar una asignación aleatoria simple (semilla: 126).

  1. Asignar a cada paciente el resultado de la aleatorización
set.seed(126)
data$Group <- sample(c("CCT group", "Hands-off group"), size = nrow(data), replace = TRUE)
  1. ¿Están balanceados los grupos respecto al total de pacientes?
table(data$Group)
## 
##       CCT group Hands-off group 
##              98             106
prop.table(table(data$Group))
## 
##       CCT group Hands-off group 
##       0.4803922       0.5196078
  1. ¿La distribución de las variables de la Tabla 1 es similar entre grupos?
# Asegurar que Gestational_age sea continua
data$Gestational_age <- as.numeric(data$Gestational_age)

# Tabla de resumen por grupo luego de la asignación aleatoria simple
tabla1_random <- data %>%
  select(Group, Nulliparous, Gestational_age, Maternal_age, Birth_weight) %>%
  tbl_summary(
    by = Group,
    type = list(c(Gestational_age, Maternal_age) ~ "continuous"),
    statistic = list(
      all_continuous() ~ "{mean} ± {sd}",
      all_categorical() ~ "{n} ({p}%)"
    ),
    digits = all_continuous() ~ 1,
    missing = "no"
  ) %>%
  add_n() %>%
  modify_header(label = "**Characteristic**") %>%
  modify_caption("**Tabla 1. Características basales por grupo: asignación aleatoria simple.**")

tabla1_random
Tabla 1. Características basales por grupo: asignación aleatoria simple.
Characteristic N CCT group
N = 98
1
Hands-off group
N = 106
1
Nulliparous 199 39 (41%) 34 (33%)
Gestational_age 204 39.0 ± 1.2 39.1 ± 1.5
Maternal_age 204 25.1 ± 5.9 26.0 ± 6.6
Birth_weight 199 3,307.0 ± 580.5 3,251.8 ± 500.0
1 n (%); Mean ± SD

Es totalmente esperable que al realizar una nueva asignación aleatoria simple (como en la tabla tabla1_random), los tamaños de los grupos (n) para cada brazo del tratamiento cambien ligeramente respecto de la primera asignación.

¿Por qué ocurre?

Cuando se utiliza asignación aleatoria simple, cada participante tiene la misma probabilidad de ser asignado a cualquier grupo, de forma independiente. Esto puede provocar:

Desequilibrios pequeños en el tamaño de los grupos, especialmente cuando el total de pacientes no es múltiplo exacto del número de grupos.

En tu ejemplo, en el grupo CCT hay 98 pacientes y en el Hands-off 106, lo cual es un desequilibrio moderado pero aceptable desde un punto de vista estadístico si se aclara el método.

¿Es un problema?

No necesariamente, pero puede afectar el poder estadístico si los grupos son muy desbalanceados.

Por eso, en estudios más rigurosos o sensibles, se usan aleatorización por bloques o estratificada, que aseguran un mejor balance en el número de sujetos por grupo (y a veces por otras variables como hospital).

Resumen:

✅ Sí, es esperable.

📊 Es una propiedad natural de la asignación aleatoria simple.

🔁 Si querés minimizar ese desequilibrio, podés usar aleatorización por bloques.

  1. ¿Están balanceados los grupos en cada hospital?
tabla2_random <- data %>%
  select(Hospital, Group) %>%
  tbl_summary(
    by = Hospital,
    statistic = list(all_categorical() ~ "{n} ({p}%)"),
    digits = all_categorical() ~ 1,
    missing = "no"
  ) %>%
  bold_labels()

as_gt(tabla2_random)
Characteristic Hospital A
N = 70
1
Hospital B
N = 134
1
Group

    CCT group 37.0 (52.9%) 61.0 (45.5%)
    Hands-off group 33.0 (47.1%) 73.0 (54.5%)
1 n (%)

⚠️ Pero al observar los datos por hospital (A y B):

Se evidencia un desequilibrio en la asignación:

Hospital A: CCT 52.9% vs. Hands-off 47.1%

Hospital B: CCT 45.5% vs. Hands-off 54.5%

Esto indica que la asignación aleatoria simple (con semilla 126) no garantizó un balance por hospital, ya que no se estratificó por ese factor.

🔍 Aunque el balance global se mantuvo (98 vs. 106 pacientes), la distribución dentro de cada hospital se vio afectada, lo cual podría introducir confusión si las características hospitalarias influyen en los desenlaces del estudio.

✅ En este contexto, habría sido preferible una aleatorización estratificada por hospital para asegurar comparabilidad entre brazos dentro de cada centro.

G. Ejercicio 4.

Asignación aleatoria por bloques permutados de tamaño 4 y 6 (semilla = 120).

set.seed(120)
asignacion_bloques <- blockrand(
  n = nrow(data),
  block.sizes = c(4, 6),
  levels = c("CCT group", "Hands-off group")
)

Verificamos

table(asignacion_bloques$treatment)
## 
##       CCT group Hands-off group 
##             106             106

Hay una asignación balanceada con aleatorización por bloques:

✔️ 106 pacientes en cada grupo (CCT group y Hands-off group).

Verificar cantidad de filas en la base

nrow(data)  # debería dar 204
## [1] 204

Si la función devuelve más de n filas, recortamos:

asignacion_bloques <- asignacion_bloques[1:nrow(data), ]

Verificamos nuevamente

nrow(data) == nrow(asignacion_bloques)  # Ahora debe ser TRUE
## [1] TRUE

Agregar a la base original

data$Group_bloques <- asignacion_bloques$treatment
  1. ¿La distribución de las variables de la Tabla 1 es similar entre grupos?
# Asegurar que Gestational_age sea continua
data$Gestational_age <- as.numeric(data$Gestational_age)

# Tabla de resumen por grupo luego de la asignación aleatoria por bloques permutados
tabla1_random_block <- data %>%
  select(Group, Nulliparous, Gestational_age, Maternal_age, Birth_weight) %>%
  tbl_summary(
    by = Group,
    type = list(c("Gestational_age", "Maternal_age") ~ "continuous"),
    statistic = list(
      all_continuous() ~ "{mean} ± {sd}",
      all_categorical() ~ "{n} ({p}%)"
    ),
    digits = all_continuous() ~ 1,
    missing = "no"
  ) %>%
  add_n() %>%
  modify_header(label = "**Characteristic**") %>%
  modify_caption("**Tabla 1. Características basales por grupo: asignación aleatoria por bloques permutados.**")

tabla1_random_block
Tabla 1. Características basales por grupo: asignación aleatoria por bloques permutados.
Characteristic N CCT group
N = 98
1
Hands-off group
N = 106
1
Nulliparous 199 39 (41%) 34 (33%)
Gestational_age 204 39.0 ± 1.2 39.1 ± 1.5
Maternal_age 204 25.1 ± 5.9 26.0 ± 6.6
Birth_weight 199 3,307.0 ± 580.5 3,251.8 ± 500.0
1 n (%); Mean ± SD

¿Están balanceados los grupos en cada hospital?

tabla2_random_block <- data %>%
  select(Hospital, Group_bloques) %>%
  tbl_summary(
    by = Hospital,
    statistic = list(all_categorical() ~ "{n} ({p}%)"),
    digits = all_categorical() ~ 1,
    missing = "no"
  ) %>%
  bold_labels()

as_gt(tabla2_random_block)
Characteristic Hospital A
N = 70
1
Hospital B
N = 134
1
Group_bloques

    CCT group 36.0 (51.4%) 67.0 (50.0%)
    Hands-off group 34.0 (48.6%) 67.0 (50.0%)
1 n (%)

La diferencia es sutil pero clara si observamos con atención:

🔍 tabla2_random (asignación aleatoria simple):

Hospital A:

CCT group: 52.9%

Hands-off group: 47.1%

Hospital B:

CCT group: 45.5%

Hands-off group: 54.5%

Esto muestra desequilibrio entre grupos dentro de cada hospital, producto de la aleatorización simple.

tabla2_random_block (asignación aleatoria por bloques):

Hospital A:

CCT group: 51.4%

Hands-off group: 48.6%

Hospital B:

Ambos grupos: 50.0%

Esto refleja un mejor equilibrio entre grupos, especialmente en el Hospital B donde está perfectamente balanceado (67 y 67). Esto demuestra que la aleatorización por bloques funciona para reducir desequilibrios, incluso cuando se estratifica por hospital.

H. Ejercicio 5.

Aleatorización estratificada por hospital y por bloques permutados.

PASO 1: Verificar cuántos pacientes hay por hospital

table(data$Hospital)
## 
## Hospital A Hospital B 
##         70        134

PASO 2: generar la asignación por separado para cada hospital, con bloques permutados y semillas distintas.

# Separar subconjuntos por hospital
data_A <- data[data$Hospital == "Hospital A", ]
data_B <- data[data$Hospital == "Hospital B", ]

# Asignación para Hospital A
set.seed(200)
asig_A <- blockrand(
  n = nrow(data_A),
  block.sizes = c(4, 6),
  levels = c("CCT group", "Hands-off group")
)

# Asignación para Hospital B
set.seed(500)
asig_B <- blockrand(
  n = nrow(data_B),
  block.sizes = c(4, 6),
  levels = c("CCT group", "Hands-off group")
)

# Recortar por si blockrand devuelve más filas
asig_A <- asig_A[1:nrow(data_A), ]
asig_B <- asig_B[1:nrow(data_B), ]

PASO 3: Unir las asignaciones a la base original

data_A$Group_estrat_bloques <- asig_A$treatment
data_B$Group_estrat_bloques <- asig_B$treatment

data_estrat <- rbind(data_A, data_B)

nrow(data_estrat) == nrow(data)  # Debe devolver TRUE
## [1] TRUE

PASO 4a: Verificar balance global

table(data_estrat$Group_estrat_bloques)
## 
##       CCT group Hands-off group 
##             101             103

PASO 4b: Verificar balance dentro de cada hospital

table(data_estrat$Hospital, data_estrat$Group_estrat_bloques)
##             
##              CCT group Hands-off group
##   Hospital A        34              36
##   Hospital B        67              67

PASO 5: Tabla 1 con asignación estratificada

# Asegurarse de que Gestational_age sea continua (por si llega como factor)
data_estrat <- data_estrat %>%
  mutate(Gestational_age = as.numeric(Gestational_age))

# Generar tabla con asignación estratificada por hospital y por bloques permutados
tabla1_estratificada <- data_estrat %>%
  select(Group_estrat_bloques, Nulliparous, Gestational_age, Maternal_age, Birth_weight) %>%
  tbl_summary(
    by = Group_estrat_bloques,
    type = list(c("Gestational_age", "Maternal_age") ~ "continuous"),
    statistic = list(
      all_continuous() ~ "{mean} ± {sd}",
      all_categorical() ~ "{n} ({p}%)"
    ),
    digits = all_continuous() ~ 1,
    missing = "no"
  ) %>%
  add_p() %>%
  add_n() %>%
  modify_header(label = "**Characteristic**") %>%
  modify_caption("**Tabla 1. Características basales por grupo: aleatorización estratificada por hospital y bloques permutados.**")

tabla1_estratificada
Tabla 1. Características basales por grupo: aleatorización estratificada por hospital y bloques permutados.
Characteristic N CCT group
N = 101
1
Hands-off group
N = 103
1
p-value2
Nulliparous 199 34 (34%) 39 (39%) 0.5
Gestational_age 204 39.0 ± 1.2 39.1 ± 1.5 0.8
Maternal_age 204 25.6 ± 6.3 25.6 ± 6.2 >0.9
Birth_weight 199 3,326.2 ± 536.7 3,230.6 ± 540.2 0.2
1 n (%); Mean ± SD
2 Pearson’s Chi-squared test; Wilcoxon rank sum test

Tabla agrupada por hospital (usando tbl_strata())

# Asegurarse de que Gestational_age sea continua
data_estrat <- data_estrat %>%
  mutate(Gestational_age = as.numeric(Gestational_age))

# Tabla agrupada por hospital con salida mergeada
tabla_estrat_por_hospital <- data_estrat %>%
  select(Hospital, Group_estrat_bloques, Nulliparous, Gestational_age, Maternal_age, Birth_weight) %>%
  tbl_strata(
    strata = Hospital,
    .tbl_fun = ~ .x %>%
      tbl_summary(
        by = Group_estrat_bloques,
        type = list(c("Gestational_age", "Maternal_age") ~ "continuous"),
        statistic = list(
          all_continuous() ~ "{mean} ± {sd}",
          all_categorical() ~ "{n} ({p}%)"
        ),
        digits = all_continuous() ~ 1,
        missing = "no"
      ) %>%
      add_p() %>%
      add_n(),
    combine_with = "tbl_merge"
  ) %>%
  modify_caption("**Tabla 2. Características basales por grupo: aleatorización estratificada por hospital.**") %>%
  bold_labels()
## The following warnings were returned during `modify_caption()`:
## ! For variable `Birth_weight` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact p-value with ties
## ! For variable `Birth_weight` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact confidence intervals with ties
## ! For variable `Gestational_age` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact p-value with ties
## ! For variable `Gestational_age` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact confidence intervals with ties
## ! For variable `Maternal_age` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact p-value with ties
## ! For variable `Maternal_age` (`Group_estrat_bloques`) and "estimate",
##   "statistic", "p.value", "conf.low", and "conf.high" statistics: cannot
##   compute exact confidence intervals with ties
tabla_estrat_por_hospital
Tabla 2. Características basales por grupo: aleatorización estratificada por hospital.
Characteristic
Hospital A
Hospital B
N CCT group
N = 34
1
Hands-off group
N = 36
1
p-value2 N CCT group
N = 67
1
Hands-off group
N = 67
1
p-value2
Nulliparous 70 8 (24%) 12 (33%) 0.4 129 26 (40%) 27 (42%) 0.8
Gestational_age 70 39.4 ± 1.2 39.5 ± 1.9 >0.9 134 38.9 ± 1.1 38.8 ± 1.2 0.6
Maternal_age 70 26.0 ± 6.5 26.6 ± 6.7 0.7 134 25.3 ± 6.3 25.0 ± 5.9 0.8
Birth_weight 69 3,487.2 ± 390.7 3,301.3 ± 435.3 0.041 130 3,241.9 ± 584.3 3,192.5 ± 588.8 >0.9
1 n (%); Mean ± SD
2 Pearson’s Chi-squared test; Wilcoxon rank sum test

¿Están balanceados los grupos en cada hospital?

✔️ Se ejecutó correctamente la aleatorización estratificada por hospital con bloques permutados de tamaño 4 y 6, utilizando semillas diferentes para cada centro.

✔️ Las asignaciones fueron integradas a la base original (data_estrat) y se verificó que el número de filas coincidiera correctamente (nrow(data_estrat) == nrow(data)).

✔️ La distribución de las variables de la Tabla 1 fue similar entre ambos grupos, sin diferencias estadísticamente significativas en los análisis globales (tbl_summary() con p-valores).

✔️ Además, se construyó una tabla estratificada por hospital usando tbl_strata(), y se observaron los siguientes resultados por centro:

  • En Hospital A, no se evidenciaron diferencias estadísticamente significativas en ninguna de las variables entre los grupos CCT y Hands-off.
  • En Hospital B, todas las comparaciones también fueron no significativas (p > 0.05), lo cual refuerza la correcta distribución de características basales entre los grupos.

📌 Aunque en el Hospital A el peso al nacer mostró una p = 0.041, esta diferencia podría atribuirse al azar y no representa un patrón sistemático, ya que no se replicó en el otro centro ni en el análisis global.

✅ En conjunto, los resultados apoyan que la aleatorización estratificada por hospital logró un adecuado balance de las variables basales, confirmando la validez del esquema de asignación.