Capítol 6: Exercicis Resolts i Cas Pràctic Integrador

Curs d’Estadística amb R per a Metges

Author

P. Loma-Osorio

Introducció al Capítol 6

Aquest capítol final recull una bateria d’exercicis resolts que repassen els conceptes i mètodes treballats al llarg del curs (Capítols 1 al 5), acompanyats d’un cas pràctic integrador final.

Per afavorir l’aprenentatge actiu, totes les solucions de codi es troben plegades per defecte. Intenta resoldre cada apartat al teu editor o consola abans de prémer el botó desplegable per comprovar la solució i les decisions metodològiques preses.

library(tidyverse)
library(readxl)
library(janitor)
library(naniar)
library(gtsummary)
library(car)
library(pROC)
library(survival)
library(survminer)
library(epitools)

Exercicis Resolts per Blocs Temàtics

Bloc 1: Fonaments d’R, Tidyverse i Importació (Capítol 1)

Exercici 1.1 (Nivell Bàsic): Gestió de vectors de marcadors cardíacs

Enunciat: Donat el següent vector de valors de troponina I ultrasensible (ng/L) mesurats a urgències: c(14, 52, 110, 8, 250, 42, 18, 550, 31, 95): 1. Calcula la mitjana, la mediana i la desviació estàndard. 2. Identifica quins pacients superen el límit de referència de la normalitat (> 14 ng/L). 3. Quants pacients presenten troponina elevada?

Mostra la solució de l’Exercici 1.1
tropo <- c(14, 52, 110, 8, 250, 42, 18, 550, 31, 95)

# 1. Paràmetres descriptius bàsics
mean(tropo)
[1] 117
Mostra la solució de l’Exercici 1.1
median(tropo)
[1] 47
Mostra la solució de l’Exercici 1.1
sd(tropo)
[1] 168.4069
Mostra la solució de l’Exercici 1.1
# 2. Vector lògic de positivitat
elevada <- tropo > 14
elevada
 [1] FALSE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
Mostra la solució de l’Exercici 1.1
# 3. Recompte de casos positius (TRUE suma 1)
sum(elevada)
[1] 8

Exercici 1.2 (Nivell Intermedi): Pipeline de depuració amb dplyr

Enunciat: Amb el conjunt de dades starwars (disponible a dplyr), filtra els personatges de gènere femení o masculí amb valors vàlids d’alçada (height) i massa (mass), selecciona les columnes rellevants, crea la columna imc (\(IMC = \frac{massa}{(alçada/100)^2}\)) arrodonida a un decimal i ordena els registres de major a menor IMC.

Mostra la solució de l’Exercici 1.2
starwars %>% 
  filter(!is.na(height), !is.na(mass), gender %in% c("masculine", "feminine")) %>% 
  select(name, height, mass, species) %>% 
  mutate(imc = round(mass / ((height / 100)^2), 1)) %>% 
  arrange(desc(imc)) %>% 
  head(5)
# A tibble: 5 × 5
  name                  height  mass species          imc
  <chr>                  <int> <dbl> <chr>          <dbl>
1 Jabba Desilijic Tiure    175  1358 Hutt           443. 
2 Dud Bolt                  94    45 Vulptereen      50.9
3 Yoda                      66    17 Yoda's species  39  
4 Owen Lars                178   120 Human           37.9
5 IG-88                    200   140 Droid           35  

Bloc 2: Estadística Descriptiva i Exploració de Dades (Capítol 2)

Exercici 2.1 (Nivell Bàsic): Freqüències creuades i totals marginals amb janitor

Enunciat: Utilitzant la cohort docent del Titanic, genera una taula de freqüències creuades de la classe de passatge (Pclass) segons el sexe (Sex), expressant els recomptes amb els percentatges per files i afegint els totals marginals tant per fila com per columna.

Mostra la solució de l’Exercici 2.1
# Cohort docent de control per reproduir els exemples
set.seed(42)
titanic_docent <- tibble(
  Sex = sample(c("female", "male"), 200, replace = TRUE, prob = c(0.55, 0.45)),
  Pclass = sample(c(1, 2, 3), 200, replace = TRUE, prob = c(0.25, 0.30, 0.45)),
  Survived = sample(c(0, 1), 200, replace = TRUE, prob = c(0.55, 0.45)),
  Age = round(rnorm(200, mean = 30, sd = 14)),
  Fare = round(rexp(200, rate = 1/35) + 5, 1)
)

# Ordre correcte: adorn_totals es calcula abans de convertir les cel·les a text
titanic_docent %>% 
  tabyl(Sex, Pclass) %>% 
  adorn_totals(c("row", "col")) %>% 
  adorn_percentages("row") %>% 
  adorn_pct_formatting(digits = 1) %>% 
  adorn_ns(position = "front")
    Sex          1          2          3        Total
 female 23 (23.2%) 26 (26.3%) 50 (50.5%)  99 (100.0%)
   male 24 (23.8%) 28 (27.7%) 49 (48.5%) 101 (100.0%)
  Total 47 (23.5%) 54 (27.0%) 99 (49.5%) 200 (100.0%)

Exercici 2.2 (Nivell Intermedi): Auditoria de missings i resum robust

Enunciat: Audita els valors absents de la base de dades clínica lung (paquet survival) amb naniar identificant les variables amb més d’un 5% de pèrdua, i resumeix la tarifa (Fare) de la cohort docent mitjançant mediana i quartils segons la classe de cabina.

Mostra la solució de l’Exercici 2.2
data(lung, package = "survival")

# 1. Auditoria de dades absents
lung %>% 
  miss_var_summary() %>% 
  filter(pct_miss > 5)
# A tibble: 2 × 3
  variable n_miss pct_miss
  <chr>     <int>    <num>
1 meal.cal     47    20.6 
2 wt.loss      14     6.14
Mostra la solució de l’Exercici 2.2
# 2. Resum robust de tarifa per asimetria
titanic_docent %>% 
  group_by(Pclass) %>% 
  summarise(
    n = n(),
    mediana_tarifa = median(Fare, na.rm = TRUE),
    q25 = quantile(Fare, 0.25, na.rm = TRUE),
    q75 = quantile(Fare, 0.75, na.rm = TRUE),
    .groups = "drop"
  )
# A tibble: 3 × 5
  Pclass     n mediana_tarifa   q25   q75
   <dbl> <int>          <dbl> <dbl> <dbl>
1      1    47           31.7  14.5  56.3
2      2    54           29.9  16.9  56.1
3      3    99           28.3  16.0  44.7

Exercici 2.3 (Nivell Avançat): Taula 1 automatitzada amb gtsummary

Enunciat: Genera una taula basal publicable de la cohort docent estratificada per supervivència (Survived), mostrant mitjana (DE) per a variables simètriques, mediana [RIC] per a variables asimètriques, n (%) per a factors, i p-valors bivariats.

Mostra la solució de l’Exercici 2.3
titanic_docent %>% 
  select(Survived, Age, Sex, Pclass, Fare) %>% 
  tbl_summary(
    by = Survived,
    statistic = list(
      Age ~ "{mean} ({sd})",
      Fare ~ "{median} ({p25}, {p75})",
      all_categorical() ~ "{n} ({p}%)"
    ),
    label = list(
      Age ~ "Edat (anys)",
      Sex ~ "Sexe",
      Pclass ~ "Classe de bitllet",
      Fare ~ "Tarifa ($)"
    ),
    missing = "no"
  ) %>% 
  add_overall() %>% 
  add_p() %>% 
  bold_labels()
Characteristic Overall
N = 2001
0
N = 1231
1
N = 771
p-value2
Edat (anys) 29 (13) 29 (13) 30 (14) 0.5
Sexe


0.3
    female 99 (50%) 57 (46%) 42 (55%)
    male 101 (51%) 66 (54%) 35 (45%)
Classe de bitllet


0.8
    1 47 (24%) 29 (24%) 18 (23%)
    2 54 (27%) 35 (28%) 19 (25%)
    3 99 (50%) 59 (48%) 40 (52%)
Tarifa ($) 29 (16, 51) 30 (18, 55) 25 (14, 46) 0.11
1 Mean (SD); n (%); Median (Q1, Q3)
2 Wilcoxon rank sum test; Pearson’s Chi-squared test

Bloc 3: Inferència i Contrast d’Hipòtesis (Capítol 3)

Exercici 3.1 (Nivell Bàsic): Taula 2x2, Chi-quadrat i Odds Ratio

Enunciat: Comprova si la diferència de proporcions de supervivència segons el sexe és estadísticament significativa mitjançant el test de Chi-quadrat i calcula l’Odds Ratio amb el seu interval de confiança del 95%.

Mostra la solució de l’Exercici 3.1
taula_2x2 <- table(titanic_docent$Sex, titanic_docent$Survived)
taula_2x2
        
          0  1
  female 57 42
  male   66 35
Mostra la solució de l’Exercici 3.1
# Contrast d'independència de Pearson
chisq.test(taula_2x2)

    Pearson's Chi-squared test with Yates' continuity correction

data:  taula_2x2
X-squared = 0.96795, df = 1, p-value = 0.3252
Mostra la solució de l’Exercici 3.1
# Odds Ratio i interval de confiança del 95%
oddsratio(taula_2x2)$measure
        odds ratio with 95% C.I.
         estimate     lower    upper
  female 1.000000        NA       NA
  male   0.721339 0.4048352 1.278793

Exercici 3.2 (Nivell Intermedi): Contrast de dues mitjanes i comprovació de supòsits

Enunciat: Avalua si existeixen diferències d’edat segons el sexe comprovant prèviament la normalitat per grups (Shapiro-Wilk) i la igualtat de variàncies (Bartlett) per justificar la tria del contrast.

Mostra la solució de l’Exercici 3.2
# 1. Comprovació de normalitat per subgrups
shapiro.test(titanic_docent$Age[titanic_docent$Sex == "male"])

    Shapiro-Wilk normality test

data:  titanic_docent$Age[titanic_docent$Sex == "male"]
W = 0.9826, p-value = 0.2053
Mostra la solució de l’Exercici 3.2
shapiro.test(titanic_docent$Age[titanic_docent$Sex == "female"])

    Shapiro-Wilk normality test

data:  titanic_docent$Age[titanic_docent$Sex == "female"]
W = 0.99341, p-value = 0.9143
Mostra la solució de l’Exercici 3.2
# 2. Homocedasticitat
bartlett.test(Age ~ Sex, data = titanic_docent)

    Bartlett test of homogeneity of variances

data:  Age by Sex
Bartlett's K-squared = 2.6193, df = 1, p-value = 0.1056
Mostra la solució de l’Exercici 3.2
# 3. Test de t de Welch (ajust per variàncies heterogènies per defecte)
t.test(Age ~ Sex, data = titanic_docent)

    Welch Two Sample t-test

data:  Age by Sex
t = 0.71393, df = 191.67, p-value = 0.4761
alternative hypothesis: true difference in means between group female and group male is not equal to 0
95 percent confidence interval:
 -2.376093  5.071963
sample estimates:
mean in group female   mean in group male 
            30.07071             28.72277 

Bloc 4: Models de Regressió Lineal i Logística (Capítol 4)

Exercici 4.1 (Nivell Intermedi): Regressió lineal múltiple i col·linealitat

Enunciat: Ajusta un model de regressió lineal múltiple per estimar la pressió sistòlica en funció de l’edat, el pes i el consum de tabac. Avalua la presència de multicol·linealitat mitjançant el VIF.

Mostra la solució de l’Exercici 4.1
# Dades sintètiques del Capítol 4
set.seed(123)
n_reg <- 100
edat_r <- sample(30:80, n_reg, replace = TRUE)
pes_r <- rnorm(n_reg, mean = 70, sd = 12)
fuma_r <- factor(sample(c(0, 1), n_reg, replace = TRUE), labels = c("No", "Sí"))
pressio_r <- 90 + 0.4 * edat_r + 0.3 * pes_r + 5 * (fuma_r == "Sí") + rnorm(n_reg, 0, 10)
dades_reg <- data.frame(pressio = pressio_r, edat = edat_r, pes = pes_r, fuma = fuma_r)

# Ajust del model lineal
mod_lin <- lm(pressio ~ edat + pes + fuma, data = dades_reg)
summary(mod_lin)

Call:
lm(formula = pressio ~ edat + pes + fuma, data = dades_reg)

Residuals:
    Min      1Q  Median      3Q     Max 
-33.278  -7.769   1.992   7.352  28.922 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 96.33731    8.11307  11.874  < 2e-16 ***
edat         0.40038    0.08162   4.905  3.8e-06 ***
pes          0.21278    0.09973   2.134   0.0354 *  
fumaSí       3.56173    2.22931   1.598   0.1134    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 10.95 on 96 degrees of freedom
Multiple R-squared:  0.2623,    Adjusted R-squared:  0.2392 
F-statistic: 11.38 on 3 and 96 DF,  p-value: 1.89e-06
Mostra la solució de l’Exercici 4.1
# Factor d'inflació de la variància (VIF < 5 descarta col·linealitat)
vif(mod_lin)
    edat      pes     fuma 
1.009844 1.006812 1.003182 

Exercici 4.2 (Nivell Avançat): Regressió logística multivariable i corba ROC

Enunciat: Modela la probabilitat de sobreviure al Titanic en funció del sexe, la classe i l’edat. Presenta els coeficients com a Odds Ratios exponenciades amb el seu IC 95% i avalua la capacitat predictiva amb l’àrea sota la corba ROC (AUC).

Mostra la solució de l’Exercici 4.2
mod_log <- glm(Survived ~ Sex + Pclass + Age, data = titanic_docent, family = binomial)

# Odds Ratios i intervals de confiança
exp(cbind(OR = coef(mod_log), confint(mod_log)))
                   OR     2.5 %   97.5 %
(Intercept) 0.4955786 0.1447972 1.645785
Sexmale     0.7275627 0.4087511 1.289145
Pclass      1.0849235 0.7592859 1.559873
Age         1.0070255 0.9852214 1.029509
Mostra la solució de l’Exercici 4.2
# Predicció de probabilitats i corba ROC
titanic_docent$prob_pred <- predict(mod_log, type = "response")
roc_obj <- roc(titanic_docent$Survived, titanic_docent$prob_pred)

plot(roc_obj, col = "#1565C0", lwd = 2, main = "Corba ROC - Model Logístic")

Mostra la solució de l’Exercici 4.2
auc(roc_obj)
Area under the curve: 0.5527

Bloc 5: Anàlisi de Supervivència (Capítol 5)

Exercici 5.1 (Nivell Intermedi): Kaplan-Meier i contrast de Log-Rank

Enunciat: Amb les dades de càncer de pulmó (lung), codifica l’objecte Surv(), calcula la mediana de supervivència global i compara les corbes segons el sexe amb el test de log-rank.

Mostra la solució de l’Exercici 5.1
lung_prep <- lung %>% 
  mutate(
    status_ev = ifelse(status == 2, 1, 0),
    sexe = factor(sex, levels = c(1, 2), labels = c("Home", "Dona"))
  )

surv_pulmo <- Surv(time = lung_prep$time, event = lung_prep$status_ev)

# Mediana global
km_global <- survfit(surv_pulmo ~ 1, data = lung_prep)
summary(km_global)$table["median"]
median 
   310 
Mostra la solució de l’Exercici 5.1
# Contrast de Log-Rank
survdiff(surv_pulmo ~ sexe, data = lung_prep)
Call:
survdiff(formula = surv_pulmo ~ sexe, data = lung_prep)

            N Observed Expected (O-E)^2/E (O-E)^2/V
sexe=Home 138      112     91.6      4.55      10.3
sexe=Dona  90       53     73.4      5.68      10.3

 Chisq= 10.3  on 1 degrees of freedom, p= 0.001 

Exercici 5.2 (Nivell Avançat): Model de Cox i riscos proporcionals

Enunciat: Ajusta un model de riscos proporcionals de Cox per estimar l’Hazard Ratio associat al sexe i a l’edat. Comprova formalment el supòsit de riscos proporcionals mitjançant els residus de Schoenfeld.

Mostra la solució de l’Exercici 5.2
mod_cox <- coxph(surv_pulmo ~ sexe + age, data = lung_prep)
summary(mod_cox)
Call:
coxph(formula = surv_pulmo ~ sexe + age, data = lung_prep)

  n= 228, number of events= 165 

              coef exp(coef)  se(coef)      z Pr(>|z|)   
sexeDona -0.513219  0.598566  0.167458 -3.065  0.00218 **
age       0.017045  1.017191  0.009223  1.848  0.06459 . 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

         exp(coef) exp(-coef) lower .95 upper .95
sexeDona    0.5986     1.6707    0.4311    0.8311
age         1.0172     0.9831    0.9990    1.0357

Concordance= 0.603  (se = 0.025 )
Likelihood ratio test= 14.12  on 2 df,   p=9e-04
Wald test            = 13.47  on 2 df,   p=0.001
Score (logrank) test = 13.72  on 2 df,   p=0.001
Mostra la solució de l’Exercici 5.2
# Test de residus de Schoenfeld
zph_test <- cox.zph(mod_cox)
print(zph_test)
       chisq df    p
sexe   2.608  1 0.11
age    0.209  1 0.65
GLOBAL 2.771  2 0.25
Mostra la solució de l’Exercici 5.2
# Diagnòstic visual
par(mfrow = c(1, 2))
plot(zph_test)

Mostra la solució de l’Exercici 5.2
par(mfrow = c(1, 1))

Cas Pràctic Integrador: Cohort de Risc Cardiovascular

Escenari Clínic

Es disposa d’un registre prospectiu hospitalari de seguiment cardiovascular (\(N = 1.200\)). Les dades en brut recollides presenten anomalies reals: valors biològicament implausibles, codis numèrics residuals d’error de picatge, cadenes de text no normalitzades i valors absents (missing data).

Preguntes Clau a Respondre:

  1. Auditoria i Neteja: Quina taxa de dades absents tenim i com esmenem els registres anòmals?
  2. Característiques Basals: Quines diferències clíniques presenten els pacients que pateixen un esdeveniment coronari respecte als que no (Taula 1)?
  3. Inferència Bivariada: Difereix la pressió arterial sistòlica segons el diagnòstic de diabetis un cop validats els supòsits?
  4. Modelització de Risc: Quin és l’efecte independent dels factors clàssics sobre l’esdeveniment coronari i quina AUC assoleix el model?
  5. Supervivència: Quina és la supervivència lliure d’esdeveniments a llarg termini i es manté l’assumpció de riscos proporcionals de Cox?

Pas 1: Generació de la Cohort Clínica Bruta

Aquest bloc reprodueix la base de dades amb les incidències habituals de recollida de dades.

Codi de simulació de la cohort bruta
set.seed(2026)
n_cohort <- 1200

raw_data <- tibble(
  `ID PACIENT` = 1001:(1000 + n_cohort),
  `Edat_Anys` = round(rnorm(n_cohort, mean = 55, sd = 10)),
  `SEX` = sample(c(1, 2, NA), n_cohort, replace = TRUE, prob = c(0.48, 0.49, 0.03)),
  `TAS mmHg` = round(rnorm(n_cohort, mean = 132, sd = 18)),
  `TAD mmHg` = round(rnorm(n_cohort, mean = 82, sd = 10)),
  `Colesterol mg/dL` = round(rnorm(n_cohort, mean = 215, sd = 42)),
  `Fuma` = sample(c(0, 1, 99), n_cohort, replace = TRUE, prob = c(0.55, 0.40, 0.05)),
  `Diabetis` = sample(c("No", "Si", "SI", "si", NA), n_cohort, replace = TRUE, prob = c(0.78, 0.08, 0.05, 0.05, 0.04)),
  `Seguiment_dies` = round(runif(n_cohort, min = 30, max = 7300)),
  `Exitus_CV` = sample(c(0, 1), n_cohort, replace = TRUE, prob = c(0.75, 0.25))
)

# Inserció deliberada d'inconsistències de registre
raw_data$`Edat_Anys`[c(15, 88)] <- c(185, -5)
raw_data$`TAS mmHg`[c(42, 102)] <- c(950, 12)
raw_data$`Colesterol mg/dL`[raw_data$`Colesterol mg/dL` > 320] <- NA

Pas 2: Pregunta 1 - Neteja, Estandardització i Auditoria de Missings

Filtra els valors fisiològicament impossibles, normalitza la variable de diabetis, recodifica els codis residuals de tabac i quantifica els patrons de pèrdua amb naniar.

Mostra la solució del Pas 1: Neteja i auditoria
cohort_clean <- raw_data %>%
  clean_names() %>%
  mutate(
    # Filtre de límits fisiològics raonables
    edat_anys = ifelse(edat_anys < 18 | edat_anys > 110, NA, edat_anys),
    tas_mm_hg = ifelse(tas_mm_hg < 60 | tas_mm_hg > 300, NA, tas_mm_hg),
    tad_mm_hg = ifelse(tad_mm_hg < 30 | tad_mm_hg > 150, NA, tad_mm_hg),
    
    # 99 corresponia a registre desconegut
    fuma = ifelse(fuma == 99, NA, fuma),
    fuma = factor(fuma, levels = c(0, 1), labels = c("No", "Sí")),
    
    # Estandardització de cadenes de text
    diabetis = str_to_lower(str_trim(diabetis)),
    diabetis = case_when(
      diabetis %in% c("si", "sí") ~ "Sí",
      diabetis == "no" ~ "No",
      TRUE ~ NA_character_
    ),
    diabetis = factor(diabetis, levels = c("No", "Sí")),
    
    # Factorització del sexe
    sex = factor(sex, levels = c(1, 2), labels = c("Home", "Dona")),
    
    # Conversió a anys per facilitar la interpretació temporal
    seguiment_anys = round(seguiment_dies / 365.25, 2)
  ) %>%
  rename(
    edat = edat_anys,
    sexe = sex,
    tas = tas_mm_hg,
    tad = tad_mm_hg,
    colesterol = colesterol_mg_d_l
  )

# Resum d'auditoria de valors absents
miss_var_summary(cohort_clean)
# A tibble: 11 × 3
   variable       n_miss pct_miss
   <chr>           <int>    <num>
 1 fuma               78    6.5  
 2 diabetis           45    3.75 
 3 sexe               27    2.25 
 4 colesterol         10    0.833
 5 edat                2    0.167
 6 tas                 2    0.167
 7 id_pacient          0    0    
 8 tad                 0    0    
 9 seguiment_dies      0    0    
10 exitus_cv           0    0    
11 seguiment_anys      0    0    
Mostra la solució del Pas 1: Neteja i auditoria
# Selecció de casos complets per a la modelització inferencial
df_estudi <- cohort_clean %>% 
  filter(!is.na(edat), !is.na(sexe), !is.na(tas), !is.na(colesterol), !is.na(fuma), !is.na(diabetis))

Pas 3: Pregunta 2 - Característiques Basals de la Cohort (Taula 1)

Construeix la Taula 1 estratificant per l’esdeveniment cardiovascular (exitus_cv) i seleccionant l’estadístic més adequat per a cada tipus de variable.

Mostra la solució del Pas 2: Taula 1
df_estudi %>%
  select(exitus_cv, edat, sexe, tas, tad, colesterol, fuma, diabetis, seguiment_anys) %>%
  tbl_summary(
    by = exitus_cv,
    statistic = list(
      all_continuous() ~ "{mean} ({sd})",
      colesterol ~ "{median} ({p25}, {p75})",
      all_categorical() ~ "{n} ({p}%)"
    ),
    label = list(
      edat ~ "Edat (anys)",
      sexe ~ "Sexe",
      tas ~ "TAS (mmHg)",
      tad ~ "TAD (mmHg)",
      colesterol ~ "Colesterol Total (mg/dL)",
      fuma ~ "Tabaquisme actiu",
      diabetis ~ "Diabetis Mellitus",
      seguiment_anys ~ "Seguiment (anys)"
    ),
    digits = all_continuous() ~ 1,
    missing = "no"
  ) %>%
  add_overall(col_label = "**Global** (N = {N})") %>%
  add_p(test = list(all_continuous() ~ "t.test", colesterol ~ "wilcox.test")) %>%
  bold_labels() %>%
  modify_spanning_header(c("stat_1", "stat_2") ~ "**Esdeveniment Cardiovascular**")
Characteristic Global (N = 1038)1
Esdeveniment Cardiovascular
p-value2
0
N = 7561
1
N = 2821
Edat (anys) 55.3 (10.0) 55.4 (10.1) 54.9 (9.6) 0.5
Sexe


0.3
    Home 529 (51%) 378 (50%) 151 (54%)
    Dona 509 (49%) 378 (50%) 131 (46%)
TAS (mmHg) 131.5 (17.4) 130.8 (17.2) 133.1 (17.6) 0.063
TAD (mmHg) 81.9 (9.8) 82.1 (9.9) 81.5 (9.7) 0.4
Colesterol Total (mg/dL) 215.0 (187.0, 241.0) 215.0 (187.0, 239.0) 215.5 (188.0, 247.0) 0.5
Tabaquisme actiu


0.089
    No 611 (59%) 457 (60%) 154 (55%)
    Sí 427 (41%) 299 (40%) 128 (45%)
Diabetis Mellitus


0.3
    No 830 (80%) 610 (81%) 220 (78%)
    Sí 208 (20%) 146 (19%) 62 (22%)
Seguiment (anys) 10.0 (5.8) 9.9 (5.8) 10.2 (5.8) 0.4
1 Mean (SD); n (%); Median (Q1, Q3)
2 Welch Two Sample t-test; Pearson’s Chi-squared test; Wilcoxon rank sum test

Pas 4: Pregunta 3 - Inferència Bivariada sobre la Pressió Arterial Sistòlica

Avalua si la TAS difereix segons el diagnòstic de diabetis verificant prèviament la normalitat visual (Q-Q plot) i l’homocedasticitat (test de Levene).

Mostra la solució del Pas 3: Inferència bivariada
# 1. Comprovació visual de normalitat per grup
par(mfrow = c(1, 2))
qqPlot(tas ~ diabetis, data = df_estudi)

Mostra la solució del Pas 3: Inferència bivariada
par(mfrow = c(1, 1))

# 2. Test d'homocedasticitat
leveneTest(tas ~ diabetis, data = df_estudi)
Levene's Test for Homogeneity of Variance (center = median)
        Df F value Pr(>F)
group    1  0.0169 0.8966
      1036               
Mostra la solució del Pas 3: Inferència bivariada
# 3. Contrast de dues mitjanes independents (t-test de Welch)
t.test(tas ~ diabetis, data = df_estudi)

    Welch Two Sample t-test

data:  tas by diabetis
t = 0.29121, df = 306.63, p-value = 0.7711
alternative hypothesis: true difference in means between group No and group Sí is not equal to 0
95 percent confidence interval:
 -2.332419  3.142683
sample estimates:
mean in group No mean in group Sí 
        131.5349         131.1298 

Pas 5: Pregunta 4 - Modelització Multivariable i Discriminació (AUC)

Ajusta una regressió logística binària multivariable sobre l’esdeveniment cardiovascular, avalua el VIF i genera la corba ROC amb el càlcul de l’AUC.

Mostra la solució del Pas 4: Model logístic i corba ROC
# 1. Ajust del model logístic
mod_cv <- glm(
  exitus_cv ~ edat + sexe + tas + colesterol + fuma + diabetis,
  data = df_estudi,
  family = binomial(link = "logit")
)

tbl_regression(mod_cv, exponentiate = TRUE) %>%
  bold_labels() %>%
  bold_p(t = 0.05)
Characteristic OR 95% CI p-value
edat 1.00 0.98, 1.01 0.5
sexe


    Home — —
    Dona 0.86 0.65, 1.13 0.3
tas 1.01 1.00, 1.02 0.064
colesterol 1.00 1.00, 1.00 0.5
fuma


    No — —
    Sí 1.26 0.96, 1.66 0.10
diabetis


    No — —
    Sí 1.18 0.84, 1.65 0.3
Abbreviations: CI = Confidence Interval, OR = Odds Ratio
Mostra la solució del Pas 4: Model logístic i corba ROC
# 2. Diagnòstic de multicol·linealitat
vif(mod_cv)
      edat       sexe        tas colesterol       fuma   diabetis 
  1.000729   1.003019   1.001830   1.002316   1.000459   1.002016 
Mostra la solució del Pas 4: Model logístic i corba ROC
# 3. Corba ROC i àrea sota la corba
df_estudi$prob_pred_cv <- predict(mod_cv, type = "response")
roc_cv <- roc(df_estudi$exitus_cv, df_estudi$prob_pred_cv)

plot(
  roc_cv,
  col = "#1565C0",
  lwd = 3,
  main = paste0("Corba ROC - Risc Cardiovascular (AUC = ", round(auc(roc_cv), 3), ")"),
  xlab = "1 - Especificitat",
  ylab = "Sensibilitat"
)
abline(a = 0, b = 1, lty = 2, col = "grey50")


Pas 6: Pregunta 5 - Anàlisi de Supervivència i Riscos Proporcionals

Construeix l’objecte Surv(), representa les corbes de Kaplan-Meier amb taula de risc per estatus diabètic, ajusta el model de Cox i valida els residus de Schoenfeld.

Mostra la solució del Pas 5: Supervivència i model de Cox
# 1. Objecte de supervivència Surv(temps, esdeveniment)
surv_cv <- Surv(time = df_estudi$seguiment_anys, event = df_estudi$exitus_cv)

# 2. Corbes de Kaplan-Meier
km_diab <- survfit(surv_cv ~ diabetis, data = df_estudi)

ggsurvplot(
  km_diab,
  data = df_estudi,
  risk.table = TRUE,
  pval = TRUE,
  conf.int = TRUE,
  palette = c("#2E7D32", "#C62828"),
  title = "Supervivència Lliure d'Esdeveniments segons Diabetis",
  xlab = "Seguiment (anys)",
  ylab = "Probabilitat de supervivència",
  risk.table.height = 0.25
)

Mostra la solució del Pas 5: Supervivència i model de Cox
# 3. Model de riscos proporcionals de Cox
mod_cox_cv <- coxph(
  surv_cv ~ edat + sexe + tas + fuma + diabetis,
  data = df_estudi
)

tbl_regression(mod_cox_cv, exponentiate = TRUE) %>%
  bold_labels() %>%
  bold_p(t = 0.05)
Characteristic HR 95% CI p-value
edat 0.99 0.98, 1.00 0.2
sexe


    Home — —
    Dona 0.94 0.74, 1.18 0.6
tas 1.00 1.00, 1.01 0.2
fuma


    No — —
    Sí 1.25 0.99, 1.58 0.062
diabetis


    No — —
    Sí 1.07 0.80, 1.42 0.6
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio
Mostra la solució del Pas 5: Supervivència i model de Cox
# 4. Verificació de riscos proporcionals (Schoenfeld)
test_schoenfeld <- cox.zph(mod_cox_cv)
print(test_schoenfeld)
           chisq df    p
edat     0.01752  1 0.89
sexe     0.95818  1 0.33
tas      0.00469  1 0.95
fuma     0.71047  1 0.40
diabetis 0.96020  1 0.33
GLOBAL   2.63453  5 0.76
Mostra la solució del Pas 5: Supervivència i model de Cox
par(mfrow = c(2, 2))
plot(test_schoenfeld)

Mostra la solució del Pas 5: Supervivència i model de Cox
par(mfrow = c(1, 1))


Síntesi Metodològica Final

  1. Gestió de la qualitat de dades: La neteja prèvia elimina biaixos per valors extrems impossibles i codis numèrics oblidats (com el valor 99). L’auditoria de valors absents garanteix la transparència del procés.
  2. Presentació de dades: La Taula 1 automatitzada amb gtsummary permet complir les directrius internacionals de publicació biomèdica (STROBE/CONSORT).
  3. Validació dels models: Tant en models logístics com en models de temps fins a l’esdeveniment (Cox), l’avaluació de supòsits (VIF i residus de Schoenfeld) és el pas indispensable abans d’interpretar els paràmetres clínics d’associació (OR i HR).