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)
data <- read.xlsx("C:/Users/Usuario/Desktop/iecs/ano_2/ensayos_clínicos/encuentro_sincrónico_del_14_06/Data_TCC.xlsx")
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)
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
| Characteristic | N | CCT group N = 1031 |
Hands-off group N = 1011 |
|---|---|---|---|
| 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.
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 = 701 |
Hospital B N = 1341 |
|---|---|---|
| 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:
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:
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.
El hospital puede actuar como un confusor o modificador de efecto. Esto se puede manejar:
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.
Realizar una asignación aleatoria simple (semilla: 126).
set.seed(126)
data$Group <- sample(c("CCT group", "Hands-off group"), size = nrow(data), replace = TRUE)
table(data$Group)
##
## CCT group Hands-off group
## 98 106
prop.table(table(data$Group))
##
## CCT group Hands-off group
## 0.4803922 0.5196078
# 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
| Characteristic | N | CCT group N = 981 |
Hands-off group N = 1061 |
|---|---|---|---|
| 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.
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 = 701 |
Hospital B N = 1341 |
|---|---|---|
| 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.
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
# 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
| Characteristic | N | CCT group N = 981 |
Hands-off group N = 1061 |
|---|---|---|---|
| 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 = 701 |
Hospital B N = 1341 |
|---|---|---|
| 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.
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
| Characteristic | N | CCT group N = 1011 |
Hands-off group N = 1031 |
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
| Characteristic |
Hospital A
|
Hospital B
|
||||||
|---|---|---|---|---|---|---|---|---|
| N | CCT group N = 341 |
Hands-off group N = 361 |
p-value2 | N | CCT group N = 671 |
Hands-off group N = 671 |
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:
📌 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.