¿Para quién es este manual?
Está diseñado para personas sin experiencia previa en R. Cada concepto estadístico se explica antes del código y los resultados se interpretan en lenguaje sencillo. El dataset utilizado es el clásico Heart Disease de Kaggle, con información clínica de 1.025 pacientes.
R es un lenguaje de programación gratuito, de código abierto y multiplataforma (Windows, macOS, Linux), especializado en análisis estadístico y visualización de datos. Fue creado en 1993 por Ross Ihaka y Robert Gentleman en la Universidad de Auckland (Nueva Zelanda).
Razones por las que R es tan popular en estadística y ciencia de datos:
RStudio es el entorno de desarrollo integrado (IDE) más usado para trabajar con R. No es R en sí mismo, sino una interfaz visual que facilita escribir código, ver gráficos y gestionar archivos.
R es el motor · RStudio es el tablero de control️
RStudio tiene cuatro paneles principales:
| Panel | Función |
|---|---|
| Editor (arriba izquierda) | Escribe y guarda scripts .R o .Rmd |
| Consola (abajo izquierda) | Ejecuta código línea a línea en tiempo real |
| Entorno (arriba derecha) | Muestra los objetos cargados en memoria |
| Archivos / Gráficos (abajo derecha) | Explora archivos, plots y ayuda |
R.version y
presiona EnterLos paquetes son extensiones que amplían R. Se
instalan una sola vez con install.packages() y se cargan en
cada sesión con library().
# Ejecuta esto UNA SOLA VEZ en la consola de RStudio
install.packages("tidyverse") # Manipulación y visualización de datos
install.packages("ggplot2") # Gráficos avanzados
install.packages("dplyr") # Transformación de datos
install.packages("knitr") # Reportes dinámicos
install.packages("rmarkdown") # Documentos RMarkdown# Esto va al INICIO de cada script — carga los paquetes instalados
library(tidyverse)
library(ggplot2)Regla de oro:
install.packages()= descargar el software (una vez).
library()= abrir el software (cada sesión).
R trabaja con diferentes tipos de objetos. Los más usados en estadística:
## [1] 25 30 45 28 52 33
# Data frame — tabla de datos (filas = observaciones, columnas = variables)
df_ejemplo <- data.frame(
nombre = c("Ana", "Luis", "María"),
edad = c(25, 30, 45),
peso = c(58.5, 75.0, 62.3)
)
df_ejemplo## [1] 35.5
## [1] 31.5
## [1] 10.63485
RMarkdown combina texto explicativo con código R en
un mismo documento, generando reportes en HTML, PDF o
Word de forma automática. Este manual fue creado con RMarkdown.
Un archivo .Rmd tiene tres partes:
---): define
título, autor y formato de salida| Variable | Tipo | Descripción |
|---|---|---|
| age | Continua | Edad del paciente (años) |
| sex | Nominal | Sexo (1 = hombre, 0 = mujer) |
| cp | Continua | Tipo de dolor en el pecho (0–3) |
| trestbps | Nominal | Presión arterial en reposo (mm Hg) |
| chol | Continua | Colesterol sérico (mg/dl) |
| fbs | Nominal | Glucosa en ayunas > 120 mg/dl (1 = sí) |
| restecg | Continua | Resultados ECG en reposo (0–2) |
| thalach | Continua | Frecuencia cardíaca máxima alcanzada |
| exang | Nominal | Angina inducida por ejercicio (1 = sí) |
| oldpeak | Continua | Depresión ST inducida por ejercicio |
| slope | Nominal | Pendiente del segmento ST pico |
| ca | Nominal | Número de vasos principales coloreados (0–3) |
| thal | Nominal | Tipo de defecto de talasemia (0–3) |
| target | Nominal | Diagnóstico de enfermedad cardíaca (1 = sí) |
if (es_html) {
datatable(datos,
caption = "Dataset Heart Disease — 1.025 pacientes",
filter = "top",
options = list(pageLength = 10, scrollX = TRUE),
rownames = FALSE)
} else {
kable(head(datos, 10),
caption = "Primeras 10 filas del dataset Heart Disease") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE, font_size = 8)
}## Filas (pacientes): 303
## Columnas (variables): 14
## Rows: 303
## Columns: 14
## $ age <int> 63, 37, 41, 56, 57, 57, 56, 44, 52, 57, 54, 48, 49, 64, 58, 5…
## $ sex <int> 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1…
## $ cp <int> 3, 2, 1, 1, 0, 0, 1, 1, 2, 2, 0, 2, 1, 3, 3, 2, 2, 3, 0, 3, 0…
## $ trestbps <int> 145, 130, 130, 120, 120, 140, 140, 120, 172, 150, 140, 130, 1…
## $ chol <int> 233, 250, 204, 236, 354, 192, 294, 263, 199, 168, 239, 275, 2…
## $ fbs <int> 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0…
## $ restecg <int> 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1…
## $ thalach <int> 150, 187, 172, 178, 163, 148, 153, 173, 162, 174, 160, 139, 1…
## $ exang <int> 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0…
## $ oldpeak <dbl> 2.3, 3.5, 1.4, 0.8, 0.6, 0.4, 1.3, 0.0, 0.5, 1.6, 1.2, 0.2, 0…
## $ slope <int> 0, 0, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 0, 2, 2, 1…
## $ ca <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0…
## $ thal <int> 1, 2, 2, 2, 2, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3…
## $ target <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
faltantes <- data.frame(
Variable = names(datos),
Faltantes = colSums(is.na(datos)),
Porcentaje = round(colSums(is.na(datos)) / nrow(datos) * 100, 2)
)
kable(faltantes, row.names = FALSE,
caption = "Resumen de valores faltantes por variable") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Variable | Faltantes | Porcentaje |
|---|---|---|
| age | 0 | 0 |
| sex | 0 | 0 |
| cp | 0 | 0 |
| trestbps | 0 | 0 |
| chol | 0 | 0 |
| fbs | 0 | 0 |
| restecg | 0 | 0 |
| thalach | 0 | 0 |
| exang | 0 | 0 |
| oldpeak | 0 | 0 |
| slope | 0 | 0 |
| ca | 0 | 0 |
| thal | 0 | 0 |
| target | 0 | 0 |
Interpretación: Si todos los valores son 0, el dataset está completo y podemos proceder sin imputación.
Las medidas de tendencia central responden a: ¿Alrededor de qué valor se concentran los datos?
La media suma todos los valores y los divide entre el número de observaciones. Es sensible a valores extremos (outliers).
\[\bar{x} = \frac{\sum_{i=1}^{n} x_i}{n}\]
variables_continuas <- c("age", "trestbps", "chol", "thalach", "oldpeak")
medias <- map_dfr(variables_continuas, function(v) {
data.frame(Variable = v, Media = round(mean(datos[[v]], na.rm = TRUE), 2))
})
kable(medias, caption = "Media aritmética de variables continuas") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Variable | Media |
|---|---|
| age | 54.37 |
| trestbps | 131.62 |
| chol | 246.26 |
| thalach | 149.65 |
| oldpeak | 1.04 |
La mediana es el valor central cuando los datos están ordenados. No se ve afectada por valores extremos.
medianas <- map_dfr(variables_continuas, function(v) {
data.frame(Variable = v, Mediana = round(median(datos[[v]], na.rm = TRUE), 2))
})
kable(medianas, caption = "Mediana de variables continuas") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Variable | Mediana |
|---|---|
| age | 55.0 |
| trestbps | 130.0 |
| chol | 240.0 |
| thalach | 153.0 |
| oldpeak | 0.8 |
La moda es el valor que aparece con mayor frecuencia.
moda_fn <- function(x) { ux <- unique(x); ux[which.max(tabulate(match(x, ux)))] }
modas <- map_dfr(variables_continuas, function(v) {
data.frame(Variable = v, Moda = moda_fn(datos[[v]]))
})
kable(modas, caption = "Moda de variables continuas") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Variable | Moda |
|---|---|
| age | 58 |
| trestbps | 120 |
| chol | 204 |
| thalach | 162 |
| oldpeak | 0 |
Las medidas de dispersión responden a: ¿Qué tan separados están los datos entre sí?
| Medida | Fórmula | Interpretación |
|---|---|---|
| Rango | Máx − Mín | Amplitud total de los datos |
| Varianza | \(s^2 = \frac{\sum(x_i - \bar{x})^2}{n-1}\) | Dispersión promedio al cuadrado |
| Desviación estándar | \(s = \sqrt{s^2}\) | Dispersión en las mismas unidades |
| Coeficiente de variación | \(CV = \frac{s}{\bar{x}} \times 100\) | Dispersión relativa en % |
dispersion <- map_dfr(variables_continuas, function(v) {
x <- datos[[v]]
data.frame(
Variable = v,
Min = round(min(x, na.rm = TRUE), 2),
Max = round(max(x, na.rm = TRUE), 2),
Rango = round(max(x, na.rm = TRUE) - min(x, na.rm = TRUE), 2),
Var = round(var(x, na.rm = TRUE), 2),
DE = round(sd(x, na.rm = TRUE), 2),
CV = paste0(round(sd(x, na.rm = TRUE) / mean(x, na.rm = TRUE) * 100, 1), "%")
)
})
kable(dispersion,
caption = "Medidas de dispersión — CV = Coeficiente de Variación") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Variable | Min | Max | Rango | Var | DE | CV |
|---|---|---|---|---|---|---|
| age | 29 | 77.0 | 48.0 | 82.48 | 9.08 | 16.7% |
| trestbps | 94 | 200.0 | 106.0 | 307.59 | 17.54 | 13.3% |
| chol | 126 | 564.0 | 438.0 | 2686.43 | 51.83 | 21% |
| thalach | 71 | 202.0 | 131.0 | 524.65 | 22.91 | 15.3% |
| oldpeak | 0 | 6.2 | 6.2 | 1.35 | 1.16 | 111.7% |
CV < 15% = poca variabilidad · CV 15–30% = variabilidad moderada · CV > 30% = alta variabilidad
## age trestbps chol thalach oldpeak
## Min. :29.00 Min. : 94.0 Min. :126.0 Min. : 71.0 Min. :0.00
## 1st Qu.:47.50 1st Qu.:120.0 1st Qu.:211.0 1st Qu.:133.5 1st Qu.:0.00
## Median :55.00 Median :130.0 Median :240.0 Median :153.0 Median :0.80
## Mean :54.37 Mean :131.6 Mean :246.3 Mean :149.6 Mean :1.04
## 3rd Qu.:61.00 3rd Qu.:140.0 3rd Qu.:274.5 3rd Qu.:166.0 3rd Qu.:1.60
## Max. :77.00 Max. :200.0 Max. :564.0 Max. :202.0 Max. :6.20
Las medidas de posición dividen un conjunto de datos ordenados en partes iguales, permitiendo ubicar un valor dentro de la distribución. Responden a la pregunta: ¿dónde se ubica este dato respecto al resto?
Los cuartiles dividen los datos (ordenados de menor a mayor) en cuatro partes iguales, cada una con el 25% de las observaciones.
\[Q_k = x_{\left(\frac{k \cdot n}{4}\right)} \quad k = 1, 2, 3\]
| Cuartil | Símbolo | Descripción |
|---|---|---|
| Primer cuartil | Q1 / P25 | 25% de los datos está por debajo |
| Segundo cuartil | Q2 / P50 | 50% de los datos está por debajo (= Mediana) |
| Tercer cuartil | Q3 / P75 | 75% de los datos está por debajo |
El P25 (Q1) y el P75 (Q3) son especialmente importantes porque:
cuartiles <- map_dfr(variables_continuas, function(v) {
x <- datos[[v]]
q <- quantile(x, probs = c(0.25, 0.50, 0.75), na.rm = TRUE)
ric <- q[3] - q[1]
data.frame(
Variable = v,
Q1_P25 = round(q[1], 2),
Q2_P50 = round(q[2], 2),
Q3_P75 = round(q[3], 2),
RIC = round(ric, 2),
Lim_inf = round(q[1] - 1.5 * ric, 2),
Lim_sup = round(q[3] + 1.5 * ric, 2)
)
})
kable(cuartiles,
col.names = c("Variable","Q1 (P25)","Q2 (P50)","Q3 (P75)",
"RIC","Límite inf. outlier","Límite sup. outlier"),
caption = "Cuartiles, Rango Intercuartílico y límites para detección de outliers") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
column_spec(5, bold = TRUE, color = col_azul)| Variable | Q1 (P25) | Q2 (P50) | Q3 (P75) | RIC | Límite inf. outlier | Límite sup. outlier | |
|---|---|---|---|---|---|---|---|
| 25%…1 | age | 47.5 | 55.0 | 61.0 | 13.5 | 27.25 | 81.25 |
| 25%…2 | trestbps | 120.0 | 130.0 | 140.0 | 20.0 | 90.00 | 170.00 |
| 25%…3 | chol | 211.0 | 240.0 | 274.5 | 63.5 | 115.75 | 369.75 |
| 25%…4 | thalach | 133.5 | 153.0 | 166.0 | 32.5 | 84.75 | 214.75 |
| 25%…5 | oldpeak | 0.0 | 0.8 | 1.6 | 1.6 | -2.40 | 4.00 |
Ejemplo de interpretación (colesterol): Si el P25 es 211 mg/dl, el 25% de los pacientes tiene colesterol igual o menor a ese valor. Un dato por encima del límite superior es un outlier y merece revisión clínica.
Los quintiles dividen los datos en cinco partes iguales (20% cada una). Son muy usados en estudios socioeconómicos (distribución del ingreso) y en epidemiología para estratificar poblaciones por nivel de riesgo.
\[Q_k = x_{\left(\frac{k \cdot n}{5}\right)} \quad k = 1, 2, 3, 4\]
quintiles <- map_dfr(variables_continuas, function(v) {
x <- datos[[v]]
q <- quantile(x, probs = seq(0.20, 0.80, by = 0.20), na.rm = TRUE)
data.frame(
Variable = v,
Q1_20 = round(q[1], 2),
Q2_40 = round(q[2], 2),
Q3_60 = round(q[3], 2),
Q4_80 = round(q[4], 2)
)
})
kable(quintiles,
col.names = c("Variable","Quintil 1 (20%)","Quintil 2 (40%)",
"Quintil 3 (60%)","Quintil 4 (80%)"),
caption = "Quintiles de las variables continuas") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE)| Variable | Quintil 1 (20%) | Quintil 2 (40%) | Quintil 3 (60%) | Quintil 4 (80%) | |
|---|---|---|---|---|---|
| 20%…1 | age | 45 | 53.00 | 58.00 | 62.0 |
| 20%…2 | trestbps | 120 | 126.00 | 134.00 | 144.0 |
| 20%…3 | chol | 204 | 230.00 | 254.00 | 285.2 |
| 20%…4 | thalach | 130 | 146.00 | 159.00 | 170.0 |
| 20%…5 | oldpeak | 0 | 0.38 | 1.12 | 1.9 |
Los percentiles dividen los datos en 100 partes iguales. El percentil \(k\) indica que el \(k\)% de los datos es menor o igual a ese valor. Los cuartiles y quintiles son casos especiales de percentiles.
percentiles_clave <- map_dfr(variables_continuas, function(v) {
x <- datos[[v]]
p <- quantile(x, probs = c(0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95),
na.rm = TRUE)
data.frame(
Variable = v,
P5 = round(p[1], 1), P10 = round(p[2], 1),
P25 = round(p[3], 1), P50 = round(p[4], 1),
P75 = round(p[5], 1), P90 = round(p[6], 1),
P95 = round(p[7], 1)
)
})
kable(percentiles_clave,
col.names = c("Variable","P5","P10","P25","P50","P75","P90","P95"),
caption = "Percentiles clave de las variables continuas") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
column_spec(c(4, 6), bold = TRUE, color = col_azul)| Variable | P5 | P10 | P25 | P50 | P75 | P90 | P95 | |
|---|---|---|---|---|---|---|---|---|
| 5%…1 | age | 39.1 | 42 | 47.5 | 55.0 | 61.0 | 66.0 | 68.0 |
| 5%…2 | trestbps | 108.0 | 110 | 120.0 | 130.0 | 140.0 | 152.0 | 160.0 |
| 5%…3 | chol | 175.0 | 188 | 211.0 | 240.0 | 274.5 | 308.8 | 326.9 |
| 5%…4 | thalach | 108.1 | 116 | 133.5 | 153.0 | 166.0 | 176.6 | 181.9 |
| 5%…5 | oldpeak | 0.0 | 0 | 0.0 | 0.8 | 1.6 | 2.8 | 3.4 |
if (es_html) {
datos %>%
select(all_of(variables_continuas)) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
plot_ly(y = ~Valor, color = ~Variable, type = "box",
colors = "Set2",
boxpoints = "outliers",
jitter = 0.3) %>%
layout(
title = "Distribución por cuartiles — los puntos son outliers",
yaxis = list(title = "Valor"),
xaxis = list(title = "Variable"),
showlegend = FALSE
)
} else {
datos %>%
select(all_of(variables_continuas)) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
ggplot(aes(x = Variable, y = Valor, fill = Variable)) +
geom_boxplot(outlier.colour = col_rojo, outlier.shape = 16,
outlier.size = 1.5, alpha = 0.7) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Distribución por cuartiles — puntos rojos = outliers",
subtitle = "Las cajas muestran Q1, mediana y Q3; bigotes hasta 1.5 x RIC",
x = "Variable", y = "Valor") +
theme_minimal() +
theme(legend.position = "none")
}datos_factor <- datos %>%
mutate(
sex = factor(sex, levels = c(0, 1), labels = c("Mujer", "Hombre")),
target = factor(target, levels = c(0, 1), labels = c("Sin enfermedad", "Con enfermedad"))
)
tabla_sexo <- datos_factor %>%
count(sex) %>%
mutate(Porcentaje = round(n / sum(n) * 100, 1),
Acumulado = cumsum(Porcentaje))
kable(tabla_sexo,
col.names = c("Sexo", "Frecuencia", "Porcentaje (%)", "Acumulado (%)"),
caption = "Distribución por sexo") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Sexo | Frecuencia | Porcentaje (%) | Acumulado (%) |
|---|---|---|---|
| Mujer | 96 | 31.7 | 31.7 |
| Hombre | 207 | 68.3 | 100.0 |
tabla_target <- datos_factor %>%
count(target) %>%
mutate(Porcentaje = round(n / sum(n) * 100, 1),
Acumulado = cumsum(Porcentaje))
kable(tabla_target,
col.names = c("Diagnóstico", "Frecuencia", "Porcentaje (%)", "Acumulado (%)"),
caption = "Distribución de la enfermedad cardíaca") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Diagnóstico | Frecuencia | Porcentaje (%) | Acumulado (%) |
|---|---|---|---|
| Sin enfermedad | 138 | 45.5 | 45.5 |
| Con enfermedad | 165 | 54.5 | 100.0 |
if (es_html) {
p_age <- plot_ly(datos, x = ~age, type = "histogram",
marker = list(color = col_azul,
line = list(color = "white", width = 0.5))) %>%
layout(title = "Distribución de Edad",
xaxis = list(title = "Edad (años)"),
yaxis = list(title = "Frecuencia"))
p_chol <- plot_ly(datos, x = ~chol, type = "histogram",
marker = list(color = col_rojo,
line = list(color = "white", width = 0.5))) %>%
layout(title = "Distribución de Colesterol",
xaxis = list(title = "Colesterol (mg/dl)"),
yaxis = list(title = "Frecuencia"))
p_age
p_chol
} else {
p1 <- ggplot(datos, aes(x = age)) +
geom_histogram(fill = col_azul, color = "white", bins = 20) +
labs(title = "Distribución de Edad",
x = "Edad (años)", y = "Frecuencia") +
theme_minimal()
p2 <- ggplot(datos, aes(x = chol)) +
geom_histogram(fill = col_rojo, color = "white", bins = 20) +
labs(title = "Distribución de Colesterol",
x = "Colesterol (mg/dl)", y = "Frecuencia") +
theme_minimal()
print(p1)
print(p2)
}La asimetría indica si los datos se concentran más a la izquierda o derecha de la media. La curtosis describe qué tan “puntiaguda” o “plana” es la distribución comparada con una normal.
| Valor | Asimetría | Curtosis |
|---|---|---|
| = 0 | Simétrica | Normal (mesocúrtica) |
| > 0 | Cola hacia la derecha | Muy puntiaguda (leptocúrtica) |
| < 0 | Cola hacia la izquierda | Muy plana (platicúrtica) |
forma <- map_dfr(variables_continuas, function(v) {
x <- datos[[v]]
data.frame(
Variable = v,
Asimetria = round(skewness(x, na.rm = TRUE), 3),
Curtosis = round(kurtosis(x, na.rm = TRUE) - 3, 3)
)
})
kable(forma, col.names = c("Variable", "Asimetría", "Curtosis (exceso)"),
caption = "Medidas de forma") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
column_spec(2, color = ifelse(abs(forma$Asimetria) > 1, "red", "black"))| Variable | Asimetría | Curtosis (exceso) |
|---|---|---|
| age | -0.201 | -0.553 |
| trestbps | 0.710 | 0.894 |
| chol | 1.138 | 4.412 |
| thalach | -0.535 | -0.081 |
| oldpeak | 1.263 | 1.530 |
El boxplot muestra la mediana, cuartiles Q1 y Q3, bigotes y valores atípicos (outliers) en un solo gráfico.
datos_long <- datos %>%
select(all_of(variables_continuas)) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor")
if (es_html) {
plot_ly(datos_long, y = ~Valor, color = ~Variable,
type = "box", colors = "Set2") %>%
layout(title = "Boxplots — Variables continuas",
yaxis = list(title = "Valor"),
xaxis = list(title = "Variable"),
showlegend = FALSE)
} else {
ggplot(datos_long, aes(x = Variable, y = Valor, fill = Variable)) +
geom_boxplot(outlier.colour = col_rojo, outlier.size = 1.5) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Boxplots — Variables continuas",
x = "Variable", y = "Valor") +
theme_minimal() +
theme(legend.position = "none")
}datos_plot <- datos %>%
mutate(target = factor(target, levels = c(0, 1),
labels = c("Sin enfermedad", "Con enfermedad")))
if (es_html) {
plot_ly(datos_plot, x = ~target, y = ~age,
split = ~target, type = "violin",
box = list(visible = TRUE),
meanline = list(visible = TRUE),
colors = c(col_azul, col_rojo)) %>%
layout(title = "Distribución de Edad según Diagnóstico",
xaxis = list(title = "Diagnóstico"),
yaxis = list(title = "Edad (años)"))
} else {
ggplot(datos_plot, aes(x = target, y = age, fill = target)) +
geom_violin(trim = FALSE, alpha = 0.7) +
geom_boxplot(width = 0.1, fill = "white") +
scale_fill_manual(values = c(col_azul, col_rojo)) +
labs(title = "Distribución de Edad según Diagnóstico",
x = "Diagnóstico", y = "Edad (años)") +
theme_minimal() +
theme(legend.position = "none")
}conteo_bar <- datos_factor %>% count(sex, target)
if (es_html) {
plot_ly(conteo_bar, x = ~sex, y = ~n, color = ~target, type = "bar",
colors = c(col_azul, col_rojo)) %>%
layout(barmode = "group",
title = "Distribución de Enfermedad Cardíaca por Sexo",
xaxis = list(title = "Sexo"),
yaxis = list(title = "Número de pacientes"),
legend = list(title = list(text = "Diagnóstico")))
} else {
ggplot(conteo_bar, aes(x = sex, y = n, fill = target)) +
geom_col(position = "dodge") +
scale_fill_manual(values = c(col_azul, col_rojo), name = "Diagnóstico") +
labs(title = "Distribución de Enfermedad Cardíaca por Sexo",
x = "Sexo", y = "Número de pacientes") +
theme_minimal()
}La correlación mide la relación lineal entre dos variables numéricas, de −1 (negativa perfecta) a +1 (positiva perfecta). Un valor cercano a 0 indica ausencia de relación lineal.
mat_cor <- cor(datos[, variables_continuas], use = "complete.obs")
kable(round(mat_cor, 3),
caption = "Matriz de correlaciones de Pearson") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE)| age | trestbps | chol | thalach | oldpeak | |
|---|---|---|---|---|---|
| age | 1.000 | 0.279 | 0.214 | -0.399 | 0.210 |
| trestbps | 0.279 | 1.000 | 0.123 | -0.047 | 0.193 |
| chol | 0.214 | 0.123 | 1.000 | -0.010 | 0.054 |
| thalach | -0.399 | -0.047 | -0.010 | 1.000 | -0.344 |
| oldpeak | 0.210 | 0.193 | 0.054 | -0.344 | 1.000 |
if (es_html) {
plot_ly(x = colnames(mat_cor), y = rownames(mat_cor), z = mat_cor,
type = "heatmap", colorscale = "RdBu", zmin = -1, zmax = 1,
text = round(mat_cor, 2), texttemplate = "%{text}") %>%
layout(title = "Mapa de Calor — Correlaciones")
} else {
corrplot(mat_cor, method = "color", type = "upper",
addCoef.col = "black", number.cex = 0.8,
tl.col = "black", tl.srt = 45,
col = colorRampPalette(c(col_rojo, "white", col_azul))(200),
title = "Correlaciones de Pearson", mar = c(0,0,1,0))
}if (es_html) {
plot_ly(datos, x = ~age, y = ~thalach,
color = ~factor(target, labels = c("Sin enfermedad","Con enfermedad")),
colors = c(col_azul, col_rojo),
type = "scatter", mode = "markers",
marker = list(opacity = 0.6, size = 7),
text = ~paste("Edad:", age, "<br>FC Máx:", thalach,
"<br>Colesterol:", chol)) %>%
layout(title = "Edad vs. Frecuencia Cardíaca Máxima",
xaxis = list(title = "Edad (años)"),
yaxis = list(title = "Frecuencia Cardíaca Máxima (lpm)"),
legend = list(title = list(text = "Diagnóstico")))
} else {
ggplot(datos, aes(x = age, y = thalach,
color = factor(target,
labels = c("Sin enfermedad","Con enfermedad")))) +
geom_point(alpha = 0.6, size = 2) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_manual(values = c(col_azul, col_rojo), name = "Diagnóstico") +
labs(title = "Edad vs. Frecuencia Cardíaca Máxima",
x = "Edad (años)", y = "Frecuencia Cardíaca Máxima (lpm)") +
theme_minimal()
}Antes de aplicar técnicas paramétricas necesitamos verificar si los datos siguen una distribución normal.
set.seed(123)
n_muestra <- min(nrow(datos), 4999)
datos_muestra <- datos[sample(nrow(datos), n_muestra), ]
shapiro_res <- map_dfr(variables_continuas, function(v) {
prueba <- shapiro.test(datos_muestra[[v]])
data.frame(
Variable = v,
Estadístico = round(prueba$statistic, 4),
p_valor = format.pval(prueba$p.value, digits = 4, eps = 0.0001),
Normalidad = ifelse(prueba$p.value > 0.05, "Normal", "No normal")
)
})
kable(shapiro_res, row.names = FALSE,
caption = "Prueba de Shapiro-Wilk (α = 0.05)") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
column_spec(4, color = ifelse(shapiro_res$Normalidad == "No normal", "red", col_verde))| Variable | Estadístico | p_valor | Normalidad |
|---|---|---|---|
| age | 0.9864 | 0.005798 | No normal |
| trestbps | 0.9659 | < 1e-04 | No normal |
| chol | 0.9469 | < 1e-04 | No normal |
| thalach | 0.9763 | < 1e-04 | No normal |
| oldpeak | 0.8442 | < 1e-04 | No normal |
ks_res <- map_dfr(variables_continuas, function(v) {
prueba <- lillie.test(datos[[v]])
data.frame(
Variable = v,
Estadístico = round(prueba$statistic, 4),
p_valor = format.pval(prueba$p.value, digits = 4, eps = 0.0001),
Normalidad = ifelse(prueba$p.value > 0.05, "Normal", "No normal")
)
})
kable(ks_res, row.names = FALSE,
caption = "Prueba de Lilliefors / KS (α = 0.05)") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
column_spec(4, color = ifelse(ks_res$Normalidad == "No normal", "red", col_verde))| Variable | Estadístico | p_valor | Normalidad |
|---|---|---|---|
| age | 0.0761 | 0.0002232 | No normal |
| trestbps | 0.1019 | < 1e-04 | No normal |
| chol | 0.0554 | 0.02543 | No normal |
| thalach | 0.0713 | 0.0007945 | No normal |
| oldpeak | 0.1853 | < 1e-04 | No normal |
El gráfico Q-Q compara cuantiles observados con los esperados bajo normalidad. Si los puntos siguen la línea diagonal, los datos son normales.
par(mfrow = c(2, 3), mar = c(4, 4, 3, 1))
for (v in variables_continuas) {
qqnorm(datos[[v]], main = paste("Q-Q —", v),
col = col_azul, pch = 16, cex = 0.6)
qqline(datos[[v]], col = col_rojo, lwd = 2)
}
par(mfrow = c(1, 1))| Aspecto | Resultado |
|---|---|
| Dataset | Heart Disease (Kaggle) |
| Pacientes | 303 |
| Variables | 14 |
| Variables continuas | 5 |
| Variables categóricas | 9 |
| Valores faltantes | 0 |
| Edad promedio | 54.4 años |
| Colesterol promedio | 246.3 mg/dl |
| % con enfermedad cardíaca | 54.5% |