¿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.


1 Introducción a R

1.1 ¿Qué es R?

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:

  • Es completamente gratuito y cuenta con una comunidad global muy activa.
  • Tiene más de 20.000 paquetes en CRAN para casi cualquier tarea estadística.
  • Produce gráficos de alta calidad para publicaciones científicas.
  • Es estándar en epidemiología, bioinformática, economía y ciencias sociales.
  • Se integra con Python, SQL, Excel y herramientas de reporte como RMarkdown.

1.2 ¿Qué es RStudio?

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

1.3 Instalación paso a paso

1.3.1 Paso 1 — Instalar R

  1. Ve a https://cran.r-project.org
  2. Haz clic en tu sistema operativo (Windows, macOS o Linux)
  3. Descarga el instalador y ejecútalo con las opciones por defecto
  4. Verifica en la consola de R: escribe R.version y presiona Enter

1.3.2 Paso 2 — Instalar RStudio

  1. Ve a https://posit.co/download/rstudio-desktop/
  2. Descarga la versión gratuita RStudio Desktop
  3. Instálala normalmente (requiere tener R instalado previamente)
  4. Abre RStudio — detectará R automáticamente

1.3.3 Paso 3 — Instalar paquetes esenciales

Los 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).

1.4 Tipos de objetos en R

R trabaja con diferentes tipos de objetos. Los más usados en estadística:

# Vector numérico — la unidad básica en R
edades <- c(25, 30, 45, 28, 52, 33)
edades
## [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
# Operaciones básicas
mean(edades)    # Media
## [1] 35.5
median(edades)  # Mediana
## [1] 31.5
sd(edades)      # Desviación estándar
## [1] 10.63485

1.5 Operadores más usados

# Asignación
x <- 5          # Forma recomendada en R

# Aritméticos
x + 2;  x - 1;  x * 3;  x / 2;  x ^ 2;  x %% 3  # módulo

# Comparación (devuelven TRUE o FALSE)
x > 3;  x == 5;  x != 4;  x >= 5

# Lógicos
TRUE & FALSE    # Y lógico  → FALSE
TRUE | FALSE    # O lógico  → TRUE
!TRUE           # Negación  → FALSE

1.6 ¿Qué es RMarkdown?

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:

  1. Encabezado YAML (entre ---): define título, autor y formato de salida
  2. Texto en Markdown: explica el análisis con formato (negritas, listas, tablas)
  3. Chunks de código: bloques de R que se ejecutan y muestran sus resultados

1.7 Bloques de código (chunks)

2 + 2                    # Suma simple
## [1] 4
sqrt(16)                 # Raíz cuadrada
## [1] 4
round(3.14159, 2)        # Redondear a 2 decimales
## [1] 3.14
paste("Hola", "Mundo")  # Concatenar texto
## [1] "Hola Mundo"

2 Carga y Exploración del Dataset

2.1 Carga de datos

url   <- "https://raw.githubusercontent.com/sharmaroshan/Heart-UCI-Dataset/master/heart.csv"
datos <- read.csv(url)
# Alternativa local: datos <- read.csv("heart.csv")

2.2 Diccionario de variables

Diccionario de variables del dataset Heart Disease
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í)

2.3 Vista de los datos

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)
}

2.4 Dimensiones y estructura

cat("Filas (pacientes):", nrow(datos), "\n")
## Filas (pacientes): 303
cat("Columnas (variables):", ncol(datos), "\n\n")
## Columnas (variables): 14
glimpse(datos)
## 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…

2.5 Valores faltantes

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)
Resumen de valores faltantes por variable
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.


3 Medidas de Tendencia Central

Las medidas de tendencia central responden a: ¿Alrededor de qué valor se concentran los datos?

3.1 Media aritmética

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)
Media aritmética de variables continuas
Variable Media
age 54.37
trestbps 131.62
chol 246.26
thalach 149.65
oldpeak 1.04

3.2 Mediana

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)
Mediana de variables continuas
Variable Mediana
age 55.0
trestbps 130.0
chol 240.0
thalach 153.0
oldpeak 0.8

3.3 Moda

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)
Moda de variables continuas
Variable Moda
age 58
trestbps 120
chol 204
thalach 162
oldpeak 0

4 Medidas de Dispersión

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)
Medidas de dispersión — CV = Coeficiente de Variación
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

4.1 Resumen estadístico completo

summary(datos[, variables_continuas])
##       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

5 Medidas de Posición

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?

5.1 Cuartiles

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

5.1.1 Importancia del P25 y P75

El P25 (Q1) y el P75 (Q3) son especialmente importantes porque:

  • Definen el Rango Intercuartílico (RIC = Q3 − Q1), que contiene el 50% central de los datos y es resistente a valores extremos.
  • Son la base del boxplot (diagrama de caja), la visualización más usada para resumir distribuciones.
  • Permiten identificar valores atípicos (outliers): un dato es outlier si está por debajo de \(Q1 - 1.5 \times RIC\) o por encima de \(Q3 + 1.5 \times RIC\).
  • En salud, el P25 y P75 se usan para interpretar tablas de crecimiento, valores de laboratorio y escalas clínicas.
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)
Cuartiles, Rango Intercuartílico y límites para detección de outliers
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.

5.2 Quintiles

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)
Quintiles de las variables continuas
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

5.3 Percentiles generales

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)
Percentiles clave de las variables continuas
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

5.4 Visualización del RIC y outliers

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")
}

6 Distribución de Frecuencias

6.1 Variables categóricas

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)
Distribución por sexo
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)
Distribución de la enfermedad cardíaca
Diagnóstico Frecuencia Porcentaje (%) Acumulado (%)
Sin enfermedad 138 45.5 45.5
Con enfermedad 165 54.5 100.0

6.2 Variables continuas — Histogramas

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)
}

7 Medidas de Forma

7.1 Asimetría (Skewness) y Curtosis (Kurtosis)

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"))
Medidas de forma
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

8 Visualizaciones Estadísticas

8.1 Boxplots

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")
}

8.2 Gráfico Violín — Edad por diagnóstico

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")
}

8.3 Gráfico de barras — Enfermedad por sexo

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()
}

9 Correlaciones

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.

9.1 Matriz de correlaciones

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)
Matriz de correlaciones de Pearson
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

9.2 Mapa de calor

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))
}

9.3 Diagrama de dispersión

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()
}

10 Pruebas de Normalidad

Antes de aplicar técnicas paramétricas necesitamos verificar si los datos siguen una distribución normal.

10.1 Prueba de Shapiro-Wilk

  • H₀: Los datos siguen una distribución normal
  • H₁: Los datos NO siguen una distribución normal
  • Regla: Si p-valor < 0.05 → rechazamos H₀
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))
Prueba de Shapiro-Wilk (α = 0.05)
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

10.2 Prueba de Kolmogorov-Smirnov (Lilliefors)

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))
Prueba de Lilliefors / KS (α = 0.05)
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

10.3 Gráficos Q-Q

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))


11 Resumen General

Resumen del análisis
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%

12 Cómo generar los outputs

12.1 HTML interactivo → RPubs

rmarkdown::render("manual_estadistica_descriptiva.Rmd",
                  output_format = "html_document")
# Luego en RStudio: Publish → RPubs

12.2 PDF estático

# Instalar TinyTeX solo una vez:
# install.packages("tinytex"); tinytex::install_tinytex()

rmarkdown::render("manual_estadistica_descriptiva.Rmd",
                  output_format = "pdf_document")

13 Referencias


Documento generado con RMarkdown • 2026
Dataset: Heart Disease — Kaggle | Licencia: CC BY 4.0