4  Gráficas con ggplot2

Parte 1: Fundamentos de graficación en R

NotaResumen

Mostramos cómo funciona ggplot2 para elaborar gráficas de puntos, de barras y de trayectorias. Utilizamos funciones de suavizamiento y algunas opciones de personalización mediante theme.

Nota

Los datos no están en el repositorio; los descargas de Dropbox y los descomprimes en una carpeta datasets/ junto a tu script (ver la presentación).

Advertencia

Si aún no cuentas con una instalación de ggplot2 dentro de R ve a la sección de instalación de paquetes o bien corre la siguiente instrucción:

install.packages("ggplot2")

4.1 ¿Qué es ggplot2?

R cuenta con su propia forma de hacer gráficas mediante el comando plot. Sin embargo, las gráficas que puedes hacer con plot son, salvo en el caso de regresiones, mucho más sencillas que las producidas por el paquete ggplot. Dicho paquete fue desarrollado inicialmente por Hadley Wickham y actualmente continúa en desarrollo por más de 340 colaboradores distintos. Por su belleza y facilidad, nosotros nos enfocaremos en gráficas con ggplot2.

Nota

Algunas de las referencias que vale la pena tengas en mente para graficar con ggplot2 son:

Wickham, Hadley. 2016. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. https://ggplot2.tidyverse.org.
Wickham, Hadley, y Garrett Grolemund. 2016. R for data science: import, tidy, transform, visualize, and model data. " O’Reilly Media, Inc."

ggplot2 es el mejor paquete para graficar.

Aprovecharemos para instalar dos paquetes que se encargan de leer bases de datos así como uno que usaremos más adelante para suavizar tendencias:

install.packages("readxl") # Para leer archivos de excel
install.packages("readr") # Para leer archivos en csv
install.packages("ggformula") # Para suavizar datos en gráficas (splines)

(No olvides limpiar todo tu ambiente de R con rm(list = ls()) antes de iniciar; tampoco olvides que estaremos trabajando un nuevo script con Ctrl+Shift+N si estás en RStudio o File > New File > RScript).

Para ello, llama al paquete utilizando el comando library:

# Librería para graficar
library(ggplot2)

# Librería para leer los datos en csv
library(readr)

Hadley Wickham, el creador de ggplot2.

ggplot trabaja con tibbles; los tibbles (pronunciados tíblz) son el formato preferido para tener tablas de datos. Probemos leer los datos de diabetes los cuales consisten en una muestra obtenida en Virginia EEUU en población afroamericana con el objetivo de determinar las prevalencias de obesidad, diabetes y otros factores de riesgo cardiovascular y proviene de la colección de bases de datos de bioestadística de Vanderbilt.

Para ello, leeremos el archivo.

Hay dos formas:

  1. Si estás en RStudio puedes irte al panel derecho superior e Import Dataset > From text (readr) Eso te escribirá un código como el siguiente:
setwd("tu/directorio")
datos_diabetes <- read_csv("datasets/diabetes.csv")
  1. la segunda forma es aprendiendo directamente que el comando es read_csv seguido del nombre del archivo. ¡Siempre puedes guardar cualquier archivo de Excel en .csv y leerlo con R así!

Para guardar un archivo de Excel en formato csv puedes utilizar F12 (Shift+Command+S en Mac) o bien, ir a File > Save As... y luego seleccionar File Format.

La base de datos se ve más o menos así:

id chol stab.glu hdl ratio glyhb location age gender height weight frame bp.1s bp.1d bp.2s bp.2d waist hip time.ppn
1000 203 82 56 3.6 4.31 Buckingham 46 female 62 121 medium 118 59 NA NA 29 38 720
1001 165 97 24 6.9 4.44 Buckingham 29 female 64 218 large 112 68 NA NA 46 48 360
1002 228 92 37 6.2 4.64 Buckingham 58 female 61 256 large 190 92 185 92 49 57 180
1003 78 93 12 6.5 4.63 Buckingham 67 male 67 119 large 110 50 NA NA 33 38 480
1005 249 90 28 8.9 7.72 Buckingham 64 male 68 183 medium 138 80 NA NA 44 41 300
1008 248 94 69 3.6 4.81 Buckingham 34 male 71 190 large 132 86 NA NA 36 42 195

Por ahora nos enfocaremos en los niveles de colesterol chol y la edad age de las personas (cada id es un individuo distinto).

4.2 Gráfica de puntos

Para graficar el comando ggplot(tus_datos) coloca la capa inicial el cual es un lienzo vacío:

# Creación de un lienzo de puntos
ggplot(datos_diabetes)

Probemos agregar puntos con geom_point especificando mediante el comando aes las variables de la base que vamos a graficar en cada uno de los ejes.

# Gráfica de puntos
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol))
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_point()`).

Advertencia

Te darás cuenta que R arroja un warning indicando que quitó una fila por valores faltantes. Esto es porque en la base de datos el id = 1281 no cuenta con mediciones de colesterol por lo cual esa fila se remueve de la gráfica (de otra forma ¿cómo graficar algo que no medimos?).

¡No olvides el +! Podemos agregar color a los geom_point con el comando color:

# Gráfica de la base de datos de diabetes
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol), color = "tomato3")

Una lista completa de los colores posibles está aquí.. También puedes usar colores RGB o en formato HEX. Por ejemplo el Manual de Identidad Gráfica del Gobierno de México establece que el tono de verde institucional es el #235B4E el cual se puede colocar tal cual.

# Gráfica de la base de datos de diabetes
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol), color = "#235B4E")

Podemos cambiar los títulos (title) de los ejes, el subtítulo (subtitle) y el pie de figura (caption) mediante labs:

# Agregamos etiquetas
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol), color = "#235B4E") +
  labs(
    x        = "Edad (años)",
    y        = "Niveles de colesterol (mg/dl)",
    title    = "Edad contra niveles de colesterol en la población afroamericana de Virginia",
    subtitle = "University of Virginia School of Medicine.",
    caption  = "Fuente: https://hbiostat.org/data/"
  )

Las gráficas cuentan con varios temas (themes) con diseños predefinidos para cambiar su imagen. Prueba los distintos: theme_classic, theme_bw, theme_gray, theme_linedraw, theme_light, theme_minimal, theme_dark y theme_void.1

1 Puedes tener aún más temas si descargas el paquete (ggthemes)[https://yutannihilation.github.io/allYourFigureAreBelongToUs/ggthemes/].

ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol), color = "#235B4E") +
  labs(
    x        = "Edad (años)",
    y        = "Niveles de colesterol (mg/dl)",
    title    = "Edad contra niveles de colesterol en la población afroamericana de Virginia",
    subtitle = "University of Virginia School of Medicine.",
    caption  = "Fuente: https://hbiostat.org/data/"
  ) +
  theme_minimal() # Agrega un tema

Para incrementar el tamaño de los puntos podemos usar el comando size dentro de geom_point:

# Agregamos etiquetas
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol), color = "#235B4E", size = 2.5) +
  labs(
    x        = "Edad (años)",
    y        = "Niveles de colesterol (mg/dl)",
    title    = "Edad contra niveles de colesterol en la población afroamericana de Virginia",
    subtitle = "University of Virginia School of Medicine.",
    caption  = "Fuente: https://hbiostat.org/data/"
  ) +
  theme_minimal() # Agrega un tema

Y si se empalman podemos hacer unos más transparentes que otros usando alpha

# Agregamos etiquetas
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol),
    color = "#235B4E",
    size = 2.5, alpha = 0.5
  ) +
  labs(
    x        = "Edad (años)",
    y        = "Niveles de colesterol (mg/dl)",
    title    = "Edad contra niveles de colesterol en la población afroamericana de Virginia",
    subtitle = "University of Virginia School of Medicine.",
    caption  = "Fuente: https://hbiostat.org/data/"
  ) +
  theme_minimal()

Finalmente, para guardar tu gráfica escribe ggsave con el nombre (sea en pdf o en png y las medidas de ancho (width) y alto (height) deseadas en pulgadas)

# width: ancho
# height: alto
ggsave("Edad_vs_colesterol.pdf", width = 8, height = 5)

El código completo para la gráfica es entonces:

# Librerías a usar
library(readr) # Leer archivos .csv
library(ggplot2) # Graficar

# Lectura de la base de datos de Virginia
datos_diabetes <- read_csv("datasets/diabetes.csv")

# Realización de la gráfica
ggplot(datos_diabetes) +
  geom_point(aes(x = age, y = chol),
    color = "#235B4E",
    size = 2.5, alpha = 0.5
  ) +
  labs(
    x        = "Edad (años)",
    y        = "Niveles de colesterol (mg/dl)",
    title    = "Edad contra niveles de colesterol en la población afroamericana de Virginia",
    subtitle = "University of Virginia School of Medicine.",
    caption  = "Fuente: https://hbiostat.org/data/"
  ) +
  theme_minimal()

# La guardamos en formato de 8 x 5 pulgadas
ggsave("Edad_vs_colesterol.pdf", width = 8, height = 5)

4.2.1 Versión con esquisse

La librería esquisse provee una interfaz más amigable para generar las gráficas y puede serte de utilidad. ¡Veámosla!

library("esquisse") # Llamamos a la librería
set_i18n("es") # Le decimos que opere en idioma español
esquisser() # Corremos el código!

Una vez hayas empezado a correr el código obtendrás una pantalla como esta:

Cuando comiensas a usar esquisse se abre una pantalla

En ella selecciona la base de datos que estamos usando (datos_diabetes) y selecciona Importar Datos

Selecciona tu base de datos

Dentro de apariencia establece el color adecuado (#235B4E) y el tema minimal.

Selecciona tu base de datos

En Etiquetas & Título elige los títulos adecuados

Selecciona tu base de datos

En Opciones de Gráfico cambia el tamaño de los puntos a 2.5

Selecciona tu base de datos

Finalmente, ve a código y copia en tu script el código correspondiente

Selecciona tu base de datos

Nota que no es exactamente igual al código de la sección anterior pero es una buena aproximación para tener una gráfica con la cual empezar.

4.2.2 R Básico

  1. Imita la siguiente gráfica de altura (height) contra peso (weight) de la base de datos de diabetes. En esta usamos otro tema distinto (classic) y los puntos son naranjas (orange) de tamaño \(0.5\):

  1. Dentro del aes puedes también aplicar funciones a las variables. Por ejemplo si queremos graficar índice de masa corporal (\(\text{peso (kg)} / (\text{altura (m)}^2)\)) podemos calcular el IMC en el mismo aes haciendo: ... aes(x = (weight * 0.453592) /(height * 0.0254)^2, ...) .... Donde el factor 0.453592 es para convertir de libras a kilos y 0.0254 de pulgadas a metros. Grafica en el eje x el índice de masa corporal (IMC) y en y el radio de colesterol definido como el nivel de colesterol chol entre el colesterol de alta densidad hdl. Obtén una gráfica como esta de color púrpura (purple), tamaño 0.5 y tema bw:

  1. Descarga y lee las bases de datasets/IMSS_BCG_2019.csv y datasets/IMSS_BCG_2020.csv las cuales contiene información de las dosis aplicadas de B.C.G (Vacuna contra la tuberculosis) contra los casos de tuberculosis detectados en el IMSS por delegación en el mismo año (2019 y 2020 respectivamente). Replica las siguientes dos gráficas con color deepskyblue3, size = 1.5. Para ello requieres instalar la librería ggrepel y usar geom_text_repel. Tu código debe tener una estructura similar a esto:
# Lectura de datos
# ...

# Gráfica
ggplot(...) +
  geom_point(...) +
  geom_text_repel(aes(
    x = `Dosis BCG`, # Así se ponen las
    y = `Casos detectados tuberculosis`, # etiquetas
    label = Del
  ), size = 2) + # de entidad
  labs(...) +
  ...

4.2.3 Ejercicio a mano: dibuja la gráfica antes de correrla

Importante

Este ejercicio es a lápiz y papel. Necesitas una hoja (de preferencia cuadriculada) y no tocar la computadora hasta el final. Suena tonto pero es el ejercicio que más rápido te enseña a leer código de ggplot2: si puedes dibujar lo que va a salir, entendiste el código.

Copia y corre solamente este bloque, que crea una base chiquita con datos hipotéticos de cinco jurisdicciones sanitarias:

jurisdicciones <- data.frame(
  jurisdiccion = c("A", "B", "C", "D", "E"),
  casos        = c(12, 30, 18, 45, 25),
  poblacion    = c(20, 50, 30, 80, 40) # en miles de habitantes
)

Ahora, sin correr nada más, dibuja en tu hoja lo que va a producir cada uno de los siguientes bloques. Para cada uno anota: (i) qué va en el eje x y qué en el y, (ii) cuántas marcas vas a ver, (iii) más o menos dónde cae cada una y (iv) si hay leyenda del lado derecho o no.

# a)
ggplot(jurisdicciones) +
  geom_point(aes(x = poblacion, y = casos))
# b) Cambiamos de lugar la x y la y
ggplot(jurisdicciones) +
  geom_point(aes(x = casos, y = poblacion))
# c) ¿De qué color van a salir los puntos y qué aparece a la derecha?
ggplot(jurisdicciones) +
  geom_point(aes(x = poblacion, y = casos, color = "red"))
# d) ¿Y ahora?
ggplot(jurisdicciones) +
  geom_point(aes(x = poblacion, y = casos), color = "red")
# e) ¿Qué le pasa al eje y? ¿Se ven los 5 puntos?
ggplot(jurisdicciones) +
  geom_point(aes(x = poblacion, y = casos)) +
  ylim(0, 20)

Ya que dibujaste las cinco, córrelas y compara con tus dibujos.

Los incisos a y b son la misma nube de puntos, sólo que reflejada: los ejes se intercambian. Es la misma información pero cuenta una historia distinta, y por eso siempre hay que decidir a conciencia qué variable va en cada eje (la convención es que en x va lo que explica y en y lo que se explica).

Los incisos c y d son la trampa más importante de todo ggplot2. En el c el color está adentro del aes, así que ggplot2 entiende que le estás pidiendo colorear según una variable; como la “variable” que le diste es el texto "red" (igual para las cinco filas), te pinta todo de un solo color (que ni siquiera es rojo, es el primer color de su paleta) y te agrega una leyenda que dice "red". En el d, el color está afuera del aes y ahí sí significa “píntame todo de rojo”, sin leyenda.

La regla: adentro del aes van las columnas de tu base; afuera del aes van los valores fijos (un color, un tamaño, una transparencia).

En el inciso e el ylim(0, 20) recorta el eje: los puntos con más de 20 casos desaparecen de la gráfica (y R te avisa con un Removed 3 rows containing missing values). Sólo se ven dos de los cinco puntos. Ojo con esto: es facilísimo publicar una gráfica que sin querer esconde la mitad de los datos.

4.2.4 Ejercicio: encuentra el error

Cada uno de los siguientes bloques quiere hacer una gráfica de puntos de poblacion contra casos con la base jurisdicciones del ejercicio anterior, pero todos están mal. Para cada uno: predice qué error va a dar, córrelo, lee el mensaje completo y arréglalo.

#a)
ggplot(jurisdicciones) geom_point(aes(x = poblacion, y = casos))
# b)
ggplot(jurisdicciones)
+geom_point(aes(x = poblacion, y = casos))
# c)
ggplot(jurisdicciones) +
  geom_point(x = poblacion, y = casos)
# d)
ggplot(jurisdicciones) +
  geom_point(aes(x = Poblacion, y = Casos))
#e)
ggplot(jurisdicciones) +
  geom_point(aes(x = poblacion; y = casos))
# f)
ggplot() +
  geom_point(jurisdicciones, aes(x = poblacion, y = casos))
  1. Falta el + entre capas. En ggplot2 cada capa se pega con un +. Correcto: ggplot(jurisdicciones) + geom_point(...).

  2. El + quedó al inicio del renglón. R lee el primer renglón, ve que ya está completo y lo ejecuta (te dibuja el lienzo vacío); luego lee el segundo y no entiende qué le estás sumando. El + siempre va al final del renglón anterior, nunca al principio del siguiente.

  3. Falta el aes. Sin él, ggplot2 no sabe que poblacion y casos son columnas de la base y las busca como si fueran variables sueltas del ambiente. Correcto: geom_point(aes(x = poblacion, y = casos)).

  4. Mayúsculas. Las columnas se llaman poblacion y casos, no Poblacion ni Casos. Si dudas de cómo se llaman exactamente, escribe names(jurisdicciones).

  5. Punto y coma en lugar de coma. Los argumentos de una función se separan con coma.

  6. Los argumentos van al revés. En los geom_ el primer argumento es el mapping (el aes), no los datos, así que hay que nombrar el de datos: geom_point(data = jurisdicciones, aes(x = poblacion, y = casos)).

4.2.5 Ejercicio con apoyo de una IA

Importante

Recuerda: nunca pegues en una IA datos identificables de personas ni bases confidenciales de tu institución. Para estos ejercicios pega el código y, si acaso, unas cuantas filas de datos inventados.

  1. Descríbela con palabras. Sin escribir código, pídele a una IA: “Escríbeme código de R con ggplot2 que haga una gráfica de puntos de la base jurisdicciones, que tiene las columnas jurisdiccion, casos y poblacion. Quiero poblacion en el eje x, casos en el eje y, puntos verdes de tamaño 3, tema minimal y título ‘Casos contra población’.” Corre lo que te dé y revisa punto por punto si cumplió las seis cosas que pediste. Anota cuáles sí y cuáles no.

  2. Que te explique. Pídele que te explique, renglón por renglón y en español sencillo, el código de la gráfica del ejercicio 3 de R Básico (la de B.C.G. con geom_text_repel). Después verifica una de sus explicaciones contra la ayuda oficial escribiendo ?geom_text_repel en la consola.

  3. Cázala inventando. Pídele: “Dame un geom_ de ggplot2 que haga una gráfica de puntos donde el tamaño del punto sea proporcional a la población, tipo mapa de burbujas.” Es probable que se invente una función. Verifica toda función que te dé escribiendo ?nombre en la consola: si R contesta No documentation for ..., no existe (o vive en un paquete que no tienes instalado). ¿Qué te inventó?

  4. Que critique tu gráfica. Toma la gráfica que hiciste en el ejercicio 2 de R Básico (IMC contra radio de colesterol), tómale captura de pantalla o descríbesela, y pregúntale: “¿Qué le falta a esta gráfica para que se entienda sola, sin que yo la explique?”. De sus sugerencias, aplica sólo las que te parezcan correctas y justifica por escrito por qué descartaste las demás.

  5. La prueba de fuego. Pídele que te genere código para una gráfica con una base de datos que tú no le diste (por ejemplo: “grafícame las coberturas de vacunación de México por año”). Córrelo. ¿De dónde sacó los datos? ¿Se los inventó? ¿Puedes rastrear la fuente? Escribe en dos renglones por qué una gráfica bonita con datos inventados es más peligrosa que no tener gráfica.

4.2.6 R Intermedio

En la base de diabetes, utiliza scale_y_continuous en particular la opción de sec.axis para agregar un segundo eje y del lado derecho donde la altura esté en metros (no pulgadas). Repite el proceso con scale_x_continuous para agregar un eje x en la parte superior con el peso en kilogramos.

4.3 Gráficas de columnas (barras)

4.3.1 Una tranquila (BCG en IMSS)

La base de datos IMSS_BCG_2019.csv contiene las dosis de B.C.G que se han aplicado en el IMSS por Delegación. Generaremos una gráfica de barras con ellas. Para eso leemos la base:

bcg <- read_csv("datasets/IMSS_BCG_2020.csv")

Ésta se ve más o menos así:

Del Dosis BCG Casos detectados tuberculosis
AGS 7945 1733
BC 10984 17618
BCS 4064 2485
CAM 2242 170
COAH 17850 5366
COL 2326 896

Graficaremos la columna de Dosis BCG por delegación Del. Para ello ponemos nuestro lienzo con ggplot y agregamos un geom_col:

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`))

Nota que aquí a la variable hubo que llamarle Dosis BCG con los acentos graves `

Esto porque de otra forma genera error pues piensa que Dosis y BCG son dos palabras distintas:

ggplot(bcg) +
  geom_col(aes(x = Del, y = Dosis BCG))
Error in parse(text = input): <text>:2:35: unexpected symbol
1: ggplot(bcg) +
2:   geom_col(aes(x = Del, y = Dosis BCG
                                     ^

Para darle color usaremos tanto color como fill. Nota que el color cambia la orilla:

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`), color = "red")

mientras que fill cambia el contenido:

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`),
    color = "red",
    fill = "blue"
  )

Cambiaré los colores por unos más bellos, y agregaré título y etiquetas a los ejes. En este caso no deseo etiqueta en el eje x por lo cual lo dejaré como comillas vacías: "".

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`),
    color = "red", fill = "blue"
  ) +
  labs(
    x        = "",
    y        = "Dosis BCG aplicadas",
    title    = "Dosis de B.C.G. aplicadas en el IMSS",
    subtitle = "Año 2020"
  )

Voy a agregar comas a los números del eje y con scale_y_continuous. El scale_y_continuous manipula lo que sea del eje y. En mi caso deseo sólo que los números les ponga comas (otra opción es para porcentajes usar percent en lugar de comma):

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`),
    color = "red", fill = "blue"
  ) +
  labs(
    x        = "",
    y        = "Dosis BCG aplicadas",
    title    = "Dosis de B.C.G. aplicadas en el IMSS",
    subtitle = "Año 2020"
  ) +
  scale_y_continuous(labels = scales::comma)

Finalmente usaremos theme para cambiar la orientación de las etiquetas del eje x a vertical (i.e con un ángulo de 90º) para que se vean mejor:

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`),
    color = "red", fill = "blue"
  ) +
  labs(
    x        = "",
    y        = "Dosis BCG aplicadas",
    title    = "Dosis de B.C.G. aplicadas en el IMSS",
    subtitle = "Año 2020"
  ) +
  scale_y_continuous(labels = scales::comma) +
  theme(axis.text.x = element_text(
    angle = 90,
    hjust = 1, vjust = 0.5
  ))

Seguro en este punto te estarás preguntando ¿y cómo Rod sabe cuál es el código para cada opción? La respuesta es que pasé mucho tiempo (¡demasiado!) en Google encontrándola pero que una vez encontré el cómo rotar las etiquetas guardé el código en mi cuaderno y ahora cada que se ofrece ¡ya sé qué poner!

Finalmente agregaré colores más lindos y un theme_linedraw

ggplot(bcg) +
  geom_col(aes(x = Del, y = `Dosis BCG`),
    color = "black", fill = "tomato3"
  ) +
  labs(
    x        = "",
    y        = "Dosis BCG aplicadas",
    title    = "Dosis de B.C.G. aplicadas en el IMSS",
    subtitle = "Año 2020"
  ) +
  scale_y_continuous(labels = scales::comma) +
  theme_linedraw() + # Nota que se agrega antes porque
  # si se pone después desconfigura la rotada
  theme(axis.text.x = element_text(
    angle = 90,
    hjust = 1, vjust = 0.5
  ))

4.3.2 Ejercicio

Utiliza la base de datos IMSS_BCG_2019.csv y genera la siguiente gráfica con color de la orilla white y relleno (fill) de color forestgreen. El tema es el bw y el texto del eje x está a 45 grados:

4.3.4 Ejercicio

  1. El siguiente código genera una base de datos de nombre vacunacion con la información de cuántos niños menores de uno o dos años tienen el esquema completo de cuatro vacunas de acuerdo con las distintas Encuestas Nacionales de Salud ENSA/ENSANUT.
Encuesta <- rep(c("2000", "2006", "2012", "2018-19", "2021"), 2)
vacperc <- c(c(0.265, 0.784, 0.742, 0.199, 0.311), c(0.5, 0.847, 0.779, 0.353, 0.358))
clase <- c(rep("1 año cumplido", 5), rep("2 años cumplidos", 5))

vacunacion <- data.frame(Encuesta, vacperc, clase)
colnames(vacunacion) <- c("ENSANUT", "Porcentaje vacunado", "Edad de los niños")

Corre el código tal cual y una vez lo hayas corrido, grafica los datos con columnas eligiendo colores y texto adecuados.

  1. Utiliza la base IMSS_BCG_2019.csv para realizar una gráfica de barras identica a la que sigue (el fill es deepskyblue4 y no tiene color). Para agregar los numeritos en las barras se usó geom_text con size = 2, hjust = 0, color = "white" y angle = 90. El tema es el bw.

4.3.5 Ejercicio a mano: dibuja las barras

Importante

A lápiz y papel otra vez. Corre nada más el bloque que crea la base y después cierra la computadora.

dosis_aplicadas <- data.frame(
  jurisdiccion = c("Norte", "Sur", "Centro"),
  dosis        = c(300, 500, 200)
)

Dibuja en tu hoja lo que produce cada bloque. Anota siempre: cuántas barras hay, en qué orden aparecen de izquierda a derecha y qué altura tiene cada una.

# a)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = jurisdiccion, y = dosis))
# b)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = dosis, y = jurisdiccion))
# c) Cuidado: aquí es geom_bar, no geom_col
ggplot(dosis_aplicadas) +
  geom_bar(aes(x = jurisdiccion))
# d)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = "", y = dosis, fill = jurisdiccion))

a) Tres barras, de alturas 200, 300 y 500… pero no en el orden en que las escribiste. ggplot2 ordena las categorías de texto alfabéticamente, así que de izquierda a derecha van Centro (200), Norte (300) y Sur (500). Si la mayoría de la gente dibujó Norte, Sur, Centro, ya aprendieron algo importante: el orden de tus barras no es el orden de tu base. Para mandar tú el orden se usa factor(jurisdiccion, levels = c("Norte", "Sur", "Centro")) dentro del aes.

b) Las mismas tres barras pero acostadas (horizontales), porque intercambiaste qué va en cada eje. Para nombres largos de jurisdicción o de entidad ésta suele ser la mejor opción: no hay que rotar el texto.

c) Tres barras todas de altura 1. Aquí está la diferencia que más confunde: geom_col grafica el valor que tú le das en la y, mientras que geom_bar cuenta cuántas filas hay de cada categoría e ignora la y. Como en tu base hay exactamente una fila por jurisdicción, todas las barras miden 1. Regla práctica: si ya tienes la cifra calculada usa geom_col; si quieres que R cuente los renglones usa geom_bar.

d) Una sola barra apilada de altura 1000 (la suma de las tres), partida en tres pedazos de colores. Al poner x = "" le dijiste que todas las jurisdicciones comparten la misma posición en x, y el fill las separa por color dentro de la misma barra.

4.3.6 Ejercicio: encuentra el error (barras)

Todos estos quieren graficar las dosis por jurisdiccion de la base anterior. Predice el error, córrelo, léelo y arréglalo:

# a)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = jurisdiccion, y = dosis), fill = jurisdiccion)
# b)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = jurisdiccion, y = dosis)) +
  labs(x = "Jurisdicción")
labs(y = "Dosis aplicadas")
# c)
ggplot(dosis_aplicadas) +
  geom_col(aes(x = jurisdiccion, y = "dosis"))
# d) Queremos las etiquetas del eje x rotadas 45 grados
ggplot(dosis_aplicadas) +
  geom_col(aes(x = jurisdiccion, y = dosis)) +
  theme(axis.text.x = element_text(angle = 45)) +
  theme_bw()
  1. El fill = jurisdiccion está afuera del aes pero jurisdiccion es una columna, no un color fijo. Las columnas van adentro del aes: geom_col(aes(x = jurisdiccion, y = dosis, fill = jurisdiccion)). (Es la misma regla del ejercicio de puntos, pero al revés.)

  2. Falta el + al final del primer labs. Sin él, R termina la gráfica ahí y luego ejecuta un segundo labs suelto que no está pegado a nada. Mejor todavía: junta los dos en uno solo, labs(x = "Jurisdicción", y = "Dosis aplicadas").

  3. "dosis" entre comillas es el texto “dosis”, no la columna. Como es el mismo texto para las tres filas, todas las barras salen iguales. Quítale las comillas.

  4. Éste no da error, y por eso es el peor: el theme_bw() va después del theme() y lo sobrescribe por completo, así que la rotación de 45 grados nunca se aplica. Los theme_algo() completos siempre van antes de los theme() con ajustes finos. Es el mismo comentario que aparece en el código de la sección de arriba.

4.4 Histogramas con ggplot2

4.4.1 Un histograma es una gráfica de barras especial

El comando geom_histogram sirve para crear histogramas. Un histograma es un tipo de gráfico de barras que acumula en cada barra conteos de cuántos cayeron en ese sitio. Para entender mejor, veamos un ejemplo. Para ello copia el siguiente código el cual genera una base de datos expuesta a continuación:

datos_histograma <- tibble(
  estaturas = c(1.42, 1.45, 1.65, 1.55, 1.32, 1.44, 1.50, 1.51, 1.62, 1.55)
)
estaturas
1.42
1.45
1.65
1.55
1.32
1.44
1.50
1.51
1.62
1.55

Viendo la base de datos notamos que puede resumirse según fueron cayendo en distintos intervalos:

Intervalo n
1.3 - 1.4 1
1.4 - 1.5 4
1.5 - 1.6 3
1.6 - 1.7 2

Podemos elaborar fácilmente una gráfica de barras para ese conteo:

donde, de nuevo, la altura de la barra corresponde al conteo de cuántos cayeron en ese intervalo.

Podemos también hacer una gráfica de barras donde no pongamos el conteo absoluto sino el porcentaje (sabemos que hay 10 mediciones en total) y esto se vería así:

Un histograma representa la misma idea que estas gráficas de barras de manera simplificada. Usando los datos asignados previamente puedo construir un histograma como sigue:

ggplot(datos_histograma) +
  geom_histogram(aes(x = estaturas),
    color = "white",
    breaks = c(1.3, 1.4, 1.5, 1.6, 1.7, 1.8)
  )

donde los breaks indican a R dónde hacer los cortes (volveremos más adelante).

Si quisiéramos verlo como porcentaje y no cómo conteo basta con agregar una opción (muy específica que a nadie se le va a ocurrir sin googlearlo) de y = (after_stat(count))/sum(after_stat(count)) en el aes como sigue:

ggplot(datos_histograma) +
  geom_histogram(aes(x = estaturas, y = (after_stat(count)) / sum(after_stat(count))),
    color = "white",
    breaks = c(1.3, 1.4, 1.5, 1.6, 1.7, 1.8)
  )

¡Hagamos entonces nuestro primer histograma!

4.4.2 Histograma de colesterol

Tomaremos de nuevo la base de diabetes que usamos en la sección anterior. Para ello la leemos:

datos_diabetes <- read_csv("datasets/diabetes.csv")

Para armar el histograma es sólo necesario especificar una variable en el aes:

# Graficación de un histograma
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol))

Date cuenta que solito R decide cuántas barras poner.

Nota

Según Ioannidis histograma viene del griego histos (mástil) y gram-ma (algo escrito). Por lo que literalmente sería algo escrito en mástiles. A pesar de que éstos se atribuyen a Karl Pearson, los histogramas fueron inventados desde antes. Él fue quien les puso nombre.

Para especificar el número de barras hay dos formas: utilizando el comando bins lo que le dice a R cuántas barras poner:

# Graficación de un histograma con 50 barras
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol), bins = 50)

O bien especificando el ancho de cada barra y dejando a R decidir:

# Graficación con barras de ancho 10
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol), binwidth = 10)

o bien especificando de dónde a dónde van los puntos de corte de cada barra:

# Graficación con barras de ancho 10
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol),
    breaks = c(50, 100, 150, 200, 250, 300, 350)
  )

El comando fill cambia el color interno de las barras.

# El comando fill cambia el color
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol), bins = 50, fill = "#cef0ff")

Mientras que el comando color cambia el color del borde de la misma:

ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  )

FInalmente, podemos hacer que el histograma muestre no el número de casos totales en la base sino la proporción de los casos mediante y = after_stat(density). La diferencia entre after_stat(density) y (after_stat(count))/sum(after_stat(count)) es que after_stat(density) está hecho para que la suma de las áreas de los rectángulos dé \(1\) mientras que en (after_stat(count))/sum(after_stat(count)) la suma de las alturas de los rectángulos es la que da \(1\).

# after_stat(density) nos da el % de casos de manera normalizada
# para que los rectángulos sumen sus áreas a área 1
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50, fill = "#cef0ff", color = "#1D1B1B"
  )

Podemos agregar, además, una densidad kernel que nos dé una idea de cómo se ve la distribución ajustada. Como veremos más adelante, la densidad kernel (aunque puede ser manipulable) es más fácil que no nos dé errores de distribución incorrecta.

# Gráfica de histograma con densidad kernel
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol))

Ojo ggplot2 funciona por capas por lo cual el orden en el que se agrega cada capa (geom) sí afecta el cómo se ve. Si ponemos el geom_density antes del geom_histogram nos queda la línea detrás del histograma:

# Gráfica de histograma con densidad kernel
ggplot(datos_diabetes) +
  geom_density(aes(x = chol)) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  )

Continuando con la edición podemos cambiar los parámetros de color y size de la distribución para ajustar su color y tamaño. Así mismo, linetype nos da una línea discontinua:

# Cambio de color y tipo de línea a la densidad
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  )

Finalmente, modificamos los temas:

# Graficación con barras de ancho 0.05
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  ) +
  labs(
    x        = "Colesterol",
    y        = "",
    title    = "Distribución observada de los niveles de colesterol",
    subtitle = "Población afroamericana de Virginia"
  ) +
  theme_bw()

La función mean calcula la media de una columna dentro de una base de datos. Podemos calcular la media de la columna de chol dentro de datos_diabetes:

# Media de la columna de colesterol
# la opción na.rm = T es para no considerar los valores missing

promedio_colesterol <- mean(datos_diabetes$chol) # No funciona pues tiene un missing
promedio_colesterol
[1] NA
# Acá le decimos que ignore el missing con na.rm = T
promedio_colesterol <- mean(datos_diabetes$chol, na.rm = T)
promedio_colesterol
[1] 207.8458

Y agregamos una línea vertical a la gráfica exactamente en la media:

# Agregamos indicadora del promedio
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  ) +
  labs(
    x        = "Colesterol",
    y        = "",
    title    = "Distribución observada de los niveles de colesterol",
    subtitle = "Población afroamericana de Virginia"
  ) +
  theme_bw() +
  geom_vline(aes(xintercept = promedio_colesterol))

Podemos cambiar el color de la línea así como el tipo:

# Agregamos indicadora del promedio
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  ) +
  labs(
    x        = "Colesterol",
    y        = "",
    title    = "Distribución observada de los niveles de colesterol",
    subtitle = "Población afroamericana de Virginia"
  ) +
  theme_bw() +
  geom_vline(aes(xintercept = promedio_colesterol),
    color = "tomato3", linetype = "dashed", linewidth = 1
  )

Finalmente, podemos agregar una anotación junto a la media que nos indique que dicha es la media. Para ello usamos el comando annotate indicando que agregaremos un texto ("text") y las coordenadas del mismo.

# Agregamos indicadora del promedio
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  ) +
  labs(
    x        = "Colesterol",
    y        = "",
    title    = "Distribución observada de los niveles de colesterol",
    subtitle = "Población afroamericana de Virginia"
  ) +
  theme_bw() +
  geom_vline(aes(xintercept = promedio_colesterol),
    color = "tomato3", linetype = "dashed", linewidth = 1
  ) +
  annotate("label",
    x = promedio_colesterol,
    y = 0.0001, label = "Media = 208", color = "tomato3"
  )

La función de scale_y_continuous permite agregar etiquetas al eje vertical (scale_x_continuous si fuera el horizontal). En este caso podemos usar porcentajes para mejor representar los datos:

# Agregamos indicadora del promedio
ggplot(datos_diabetes) +
  geom_histogram(aes(x = chol, y = after_stat(density)),
    bins = 50,
    fill = "#cef0ff", color = "#1D1B1B"
  ) +
  geom_density(aes(x = chol),
    color = "deepskyblue4",
    linetype = "dotted", linewidth = 1
  ) +
  labs(
    x        = "Colesterol",
    y        = "",
    title    = "Distribución observada de los niveles de colesterol",
    subtitle = "Población afroamericana de Virginia"
  ) +
  theme_bw() +
  geom_vline(aes(xintercept = promedio_colesterol),
    color = "tomato3", linetype = "dashed", linewidth = 1
  ) +
  annotate("label",
    x = promedio_colesterol,
    y = 0.0001, label = "Media = 208", color = "tomato3"
  ) +
  scale_y_continuous(labels = scales::percent)

4.4.3 Ejercicios

  1. Utiliza la función quantile (con la opción na.rm = T) para calcular los cuantiles 0.975 y 0.025 y agrega dos líneas verticales a la gráfica anterior indicando los valores de los cuantiles junto con sus etiquetas:

  1. Imita la siguiente gráfica de altura en pulgadas (height) con color red donde se usó el comando fill para colorear el interior de la densidad en azul (deepskyblue4) y el tema theme_linedraw.

Nota quizá necesites modificar la transparencia con alpha.

  1. Considera la siguiente base de datos:
mis.datos <- data.frame(
  x = c(
    1.03, 1.24, 1.47, 1.52, 1.92, 1.93, 1.94, 1.95, 1.96, 1.97,
    1.98, 1.99, 2.72, 2.75, 2.78, 2.81, 2.84, 2.87, 2.90, 2.93,
    2.96, 2.99, 3.60, 3.64, 3.66, 3.72, 3.77, 3.88, 3.91, 4.14,
    4.54, 4.77, 4.81, 5.62
  )
)

Realiza un histograma de la variable x dentro de mis.datos con binwidth a 0.75:

  1. ¿Podemos concluir la forma de la distribución a partir del histograma? Es decir ¿está centrada?
Tip

Minitab tiene un blog sobre cuándo usar histogramas. ¿Puedes descubrir todos sus errores?

  1. Realiza el mismo histograma pero ahora con un ancho de banda de 1.2. ¿Y ahora?
  1. Considera la siguiente base de datos:
mis.datos <- data.frame(
  A = c(
    3.15, 5.46, 3.28, 4.20, 1.98, 2.28, 3.12, 4.10, 3.42, 3.91,
    2.06, 5.53, 5.19, 2.39, 1.88, 3.43, 5.51, 2.54, 3.64, 4.33,
    4.85, 5.56, 1.89, 4.84, 5.74, 3.22, 5.52, 1.84, 4.31, 2.01,
    4.01, 5.31, 2.56, 5.11, 2.58, 4.43, 4.96, 1.90, 5.60, 1.92
  ),
  B = c(
    2.90, 5.21, 3.03, 3.95, 1.73, 2.03, 2.87, 3.85, 3.17, 3.66,
    1.81, 5.28, 4.94, 2.14, 1.63, 3.18, 5.26, 2.29, 3.39, 4.08,
    4.60, 5.31, 1.64, 4.59, 5.49, 2.97, 5.27, 1.59, 4.06, 1.76,
    3.76, 5.06, 2.31, 4.86, 2.33, 4.18, 4.71, 1.65, 5.35, 1.67
  ),
  C = c(
    2.65, 4.96, 2.78, 3.70, 1.48, 1.78, 2.62, 3.60, 2.92, 3.41,
    1.56, 5.03, 4.69, 1.89, 1.38, 2.93, 5.01, 2.04, 3.14, 3.83,
    4.35, 5.06, 1.39, 4.34, 5.24, 2.72, 5.02, 1.34, 3.81, 1.51,
    3.51, 4.81, 2.06, 4.61, 2.08, 3.93, 4.46, 1.4, 5.1, 1.42
  ),
  D = c(
    2.40, 4.71, 2.53, 3.45, 1.23, 1.53, 2.37, 3.35, 2.67, 3.16,
    1.31, 4.78, 4.44, 1.64, 1.13, 2.68, 4.76, 1.79, 2.89, 3.58,
    4.10, 4.81, 1.14, 4.09, 4.99, 2.47, 4.77, 1.09, 3.56, 1.26,
    3.26, 4.56, 1.81, 4.36, 1.83, 3.68, 4.21, 1.15, 4.85, 1.17
  )
)

Grafica un histograma de las variables A, B, C y D de dicha base con un ancho de banda (binwidth) igual a 1:

¿Podemos concluir la forma de la distribución a partir del histograma? Es decir ¿hay distribuciones sesgadas a la izquierda, a la derecha, uniformes, centradas o con colas pesadas?

Realiza el mismo histograma pero ahora con un ancho de banda de 0.25 ¿por qué hubo cambios? Analiza la base de datos (los valores en función de la columna A) y concluye.

4.4.4 Ejercicio a mano: construye el histograma con lápiz

Importante

Este ejercicio es completamente a mano. No corras nada hasta el último punto.

Éstas son las edades (hipotéticas) de 12 personas que acudieron a una jornada de vacunación:

edades <- data.frame(
  edad = c(23, 27, 31, 34, 35, 36, 38, 41, 42, 44, 52, 58)
)
  1. Cuenta a mano. Llena esta tabla de conteos (una raya por cada persona que caiga en cada intervalo). El intervalo [20, 30) incluye al 20 pero no al 30:

    Intervalo Rayitas Total
    [20, 30)
    [30, 40)
    [40, 50)
    [50, 60)
    Suma
  2. Dibújalo. Con esa tabla, dibuja el histograma en tu hoja: el eje x va de 20 a 60 y el eje y de 0 al total más grande que hayas contado. ¿Cuál es la barra más alta? ¿Hacia dónde está cargada la distribución?

  3. Predice qué cambia. Sin dibujarlo, contesta: si en vez de intervalos de 10 años usaras intervalos de 5 años, ¿cuántas barras tendrías? ¿la barra más alta seguiría teniendo la misma altura? ¿por qué?

  4. Ahora sí, comprueba. Corre el código y compara con tus dibujos:

ggplot(edades) +
  geom_histogram(aes(x = edad),
    binwidth = 10, boundary = 20,
    fill = "deepskyblue3", color = "white"
  ) +
  theme_bw()

Los conteos son: [20,30) = 2 (23 y 27), [30,40) = 5 (31, 34, 35, 36 y 38), [40,50) = 3 (41, 42 y 44) y [50,60) = 2 (52 y 58). Suman 12, que es justo cuánta gente hay: si tu suma no da 12, contaste mal. Esa verificación siempre se puede hacer y siempre conviene hacerla.

Con intervalos de 5 años tendrías el doble de barras (8 en lugar de 4) y todas serían más bajitas o iguales, porque las mismas 12 personas ahora se reparten en más cajitas. La barra de [30,40) que medía 5 se parte en [30,35) = 2 y [35,40) = 3:

La moraleja: el ancho del intervalo no es un detalle estético. Los mismos 12 datos pueden verse concentrados o dispersos según cómo los cortes, y con 12 observaciones ningún histograma te va a decir gran cosa sobre la forma de la distribución. Desconfía de cualquier histograma cuyo ancho de banda no se justifique.

4.4.5 Ejercicio: encuentra el error (histogramas)

Predice el error, córrelo, léelo y arréglalo. Usa la base edades del ejercicio anterior:

# a) Queremos un histograma de las edades
ggplot(edades) +
  geom_histogram(aes(x = edad, y = edad))
# b) Queremos barras de ancho 5
ggplot(edades) +
  geom_histogram(aes(x = edad), binwidth = 5, bins = 30)
# c) Queremos el histograma en porcentaje
ggplot(edades) +
  geom_histogram(aes(x = edad, y = porcentaje))
# d) Queremos un histograma de la jurisdicción
ggplot(dosis_aplicadas) +
  geom_histogram(aes(x = jurisdiccion))
  1. Un histograma calcula solo el eje y (es el conteo). No hay que dárselo, y menos la misma variable de la x. Correcto: geom_histogram(aes(x = edad)).

  2. binwidth y bins son dos formas distintas de decir lo mismo (binwidth es el ancho de cada barra, bins es cuántas barras quieres). Si le das las dos, R te avisa que va a ignorar una. Deja sólo binwidth = 5.

  3. porcentaje no es una columna de la base ni una variable calculada. Para eso ggplot2 tiene variables internas que se piden con after_stat: aes(x = edad, y = after_stat(count) / sum(after_stat(count))). (Si te topas con código viejo de ggplot2 vas a ver esto mismo escrito como ..count.. y ..density.., entre puntos. Esa forma todavía funciona pero ya está marcada como obsoleta: si la usas, R te va a llenar la consola de advertencias.)

  4. jurisdiccion es texto, y un histograma sólo funciona con variables numéricas: no se puede preguntar cuántas jurisdicciones caen entre “Centro” y “Norte”. Para contar categorías se usa geom_bar, que es justo lo que viste en el ejercicio anterior.

4.4.6 Ejercicio con apoyo de una IA (histogramas)

  1. Que elija por ti y desconfía. Pídele a una IA: “Tengo 12 edades y quiero hacer un histograma en ggplot2. ¿Qué ancho de banda me recomiendas y por qué?”. Después pregúntale lo mismo pero diciéndole que tienes 12,000 edades. ¿Cambió su recomendación? ¿Te dio una razón o sólo un número? Contrasta su respuesta con lo que tú ya concluiste en el ejercicio a mano.

  2. El histograma tramposo. Pídele: “Dame código de ggplot2 para un histograma que haga ver que la mayoría de la población es joven, usando la base edades. Corre lo que te dé y compáralo con el histograma honesto que hiciste tú. ¿Qué manipuló: el ancho de banda, los límites de los ejes, la escala? Escribe en dos renglones cómo detectarías esa misma manipulación en una gráfica que te llegue en un informe.

  3. Que traduzca la gráfica a palabras. Descríbele (o mándale captura de) el histograma de colesterol de esta sección y pídele que te escriba el pie de figura de un artículo. Revisa que no invente cifras, que no diga “significativo” sin prueba estadística de por medio y que mencione la fuente de los datos. Corrige lo que haga falta y quédate con tu versión.

4.5 Gráficas de trayectorias

En ggplot2 puedes graficar lo que quieras. Hasta puedes declarar tu amor. Este próximo 14 de febrero usa ggplot2.

La base de datos casos_covid_agosto_2022.csv contiene datos sobre la cantidad de casos reportados en el Sistema Nacional de Vigilancia Epidemiológica (SINAVE) por covid, sospecha de covid u otra enfermedad respiratoria. Leamos la base:

Grafiquemos la cantidad de casos totales en México como puntos

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff")

podemos agregar una línea que una los puntos pero como hay tantos no se ve muy bien:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_line(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d")

podemos mejor usar la función geom_spline del paquete ggformula para usar una línea que se encargue de eliminar el ruido de los datos y descubrir la forma inherente:

library(ggformula)

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d")

nota que el suavizamiento ahora es mucho mejor. Podemos controlar el nivel de suavizamiento con los grados de libertad del spline df:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n, color = "df 900"), df = 900) +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n, color = "df 45"), df = 45) +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n, color = "df 20"), df = 15) +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n, color = "df 5"), df = 10)

Hay un equlibrio en el nivel de suavizamiento: poner demasiado poco rompe la forma df = 10 mientras que suavizamiento alto sigue la tendencia de los datos tal cual df = 900. Me quedaré con df = 45 porque me gusta cómo se ve a ese nivel.

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45)

Lo que sigue es personalizar la gráfica poniendo los títulos, el tema y cambiando los ejes a tener notación con comas. Todo esto lo hemos hecho antes:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma())

Agregaré un scale_x_date para elegir tener cortes en el eje x cada 6 meses y:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  scale_x_date(
    date_breaks = "6 months", date_minor_breaks = "3 months",
    date_labels = "%b/%Y"
  ) +
  theme_bw()

Utilizaremos theme para realizar cambios a cada parte de la gráfica comenzando por cambiar el color de fondo:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  scale_x_date(
    date_breaks = "6 months", date_minor_breaks = "3 months",
    date_labels = "%b/%Y"
  ) +
  theme_classic() +
  theme(
    panel.background = element_rect(fill = "#320d3e")
  )

theme nos permite cambiar las características de cualquier parte del plot. Por ejemplo haciendo una mezcla de colores horribles podemos hacer la gráfica más fea que hayas visto:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#f162ff") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  scale_x_date(
    date_breaks = "6 months", date_minor_breaks = "3 months",
    date_labels = "%b/%Y"
  ) +
  theme_classic() +
  theme(
    panel.background = element_rect(fill = "#320d3e"), # Fondo de la gráfica
    plot.background  = element_rect(fill = "green"), # Fondo de los títulos
    axis.text.x      = element_text(color = "red", size = 5), # Letras de los ejes
    axis.text.y      = element_text(color = "forestgreen"), # Letras de los ejes
    axis.ticks       = element_line(color = "red", linewidth = 2), # Los | abajo de los ejes
    axis.line.x      = element_line(color = "orange", linewidth = 2), # Línea del eje
    axis.line.y      = element_line(color = "yellow", linewidth = 3), # Otra línea del eje
    plot.title       = element_text(color = "blue", size = 20), # Título
    plot.subtitle    = element_text(color = "gray25", face = "italic"), # Subtítulo
    plot.caption     = element_text(color = "purple"), # Caption
    axis.title.y     = element_text(color = "pink"), # Lo que dice el eje y
  )

La cual podemos arreglar rápidamente con una mezcla amigable de colores:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#007677") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  scale_x_date(
    date_breaks = "6 months", date_minor_breaks = "3 months",
    date_labels = "%b/%Y"
  ) +
  theme_classic() +
  theme(
    panel.background = element_rect(fill = "#003f5c"), # Fondo de la gráfica
    plot.background  = element_rect(fill = "#003f5c", color = "black"), # Fondo de los títulos
    axis.text.x      = element_text(color = "gray75"), # Letras de los ejes
    axis.text.y      = element_text(color = "gray75"), # Letras de los ejes
    axis.ticks       = element_line(color = "gray75"), # Los | abajo de los ejes
    axis.line.x      = element_line(color = "gray75"), # Línea del eje
    axis.line.y      = element_line(color = "gray75"), # Otra línea del eje
    plot.title       = element_text(color = "gray75", size = 25), # Título
    plot.subtitle    = element_text(color = "#62a757", face = "italic"), # Subtítulo
    plot.caption     = element_text(color = "#62a757"), # Caption
    axis.title.y     = element_text(color = "gray75"), # Lo que dice el eje y
  )

y finalmente podemos agregar anotaciones:

ggplot(sinave) +
  geom_point(aes(x = FECHA_SINTOMAS, y = n), color = "#007677") +
  geom_spline(aes(x = FECHA_SINTOMAS, y = n), color = "#ffd79d", df = 45) +
  labs(
    x        = "",
    y        = "Casos incidentes",
    title    = "Enfermedades Respiratorias",
    subtitle = "Registros del Sistema Nacional de Vigilancia Epidemiológica (SINAVE)",
    caption  = "Datos Abiertos de la Dirección General de Epidemiología"
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  scale_x_date(
    date_breaks = "6 months", date_minor_breaks = "3 months",
    date_labels = "%b/%Y"
  ) +
  theme_classic() +
  theme(
    panel.background = element_rect(fill = "#003f5c"), # Fondo de la gráfica
    plot.background  = element_rect(fill = "#003f5c", color = "black"), # Fondo de los títulos
    axis.text.x      = element_text(color = "gray75"), # Letras de los ejes
    axis.text.y      = element_text(color = "gray75"), # Letras de los ejes
    axis.ticks       = element_line(color = "gray75"), # Los | abajo de los ejes
    axis.line.x      = element_line(color = "gray75"), # Línea del eje
    axis.line.y      = element_line(color = "gray75"), # Otra línea del eje
    plot.title       = element_text(color = "gray75", size = 25), # Título
    plot.subtitle    = element_text(color = "#62a757", face = "italic"), # Subtítulo
    plot.caption     = element_text(color = "#62a757"), # Caption
    axis.title.y     = element_text(color = "gray75"), # Lo que dice el eje y
  ) +
  # Anotaciones
  geom_point(aes(x = as.Date("2020/02/28"), y = 1), color = "gray75", data = NULL, size = 2) +
  geom_segment(aes(
    x = as.Date("2020/02/28"), xend = as.Date("2020/02/28"), y = 1,
    yend = 75000
  ), color = "gray75") +
  geom_point(aes(x = as.Date("2020/02/28"), y = 75000), color = "gray75", data = NULL, size = 2) +
  annotate("text",
    x = as.Date("2020/03/15"), y = 75000, hjust = 0,
    label = "Primer caso de COVID-19", color = "gray75"
  )

4.5.1 Ejercicios

  1. Agrega a la gráfica anterior una anotación el 24 de diciembre del 2020 indicando que ese día se aplicó la primera vacuna contra COVID-19 en México.

  2. Cambia el tema de la gráfica anterior por cualquier otra combinación de cuatro colores (en lugar del verde, azul, amarillo y gris)

  3. La base de datos dosis_antihep_jalisco.csv contiene la cantidad de dosis aplicadas de producto biológico Antihepatitis b en el IMSS el 2000 al 2020. Replica la siguiente gráfica lo más cercano posible. Los colores usados son "#2C3333" para el fondo, "#A5C9CA" y "#ADADFF" para puntos, texto del eje, títulos y spline y "#395B64" para los ejes.

4.5.2 Ejercicio a mano: dibuja la trayectoria

Importante

A lápiz y papel. Corre sólo el bloque de la base y guarda la computadora.

brote <- data.frame(
  semana = c(1, 2, 3, 4, 5, 6),
  casos  = c(10, 40, 90, 60, 30, 10)
)

Dibuja lo que sale de cada bloque. Anota cuántos picos tiene la curva, en qué semana está el máximo y si se ven o no los puntos individuales:

# a)
ggplot(brote) +
  geom_line(aes(x = semana, y = casos))
# b)
ggplot(brote) +
  geom_line(aes(x = semana, y = casos)) +
  geom_point(aes(x = semana, y = casos))
# c) ¿Qué gráfica sale de los MISMOS datos pero en desorden?
brote_desordenado <- brote[c(3, 1, 5, 2, 6, 4), ]

ggplot(brote_desordenado) +
  geom_line(aes(x = semana, y = casos))
# d) La misma base desordenada, pero con geom_path en vez de geom_line
ggplot(brote_desordenado) +
  geom_path(aes(x = semana, y = casos))

a) y b) son la misma curva de brote: sube de 10 a 90 hasta la semana 3 y luego baja hasta volver a 10 en la semana 6. La única diferencia es que en b se ven los seis puntos marcados encima de la línea. Poner los puntos casi siempre vale la pena: le dice a quien lee dónde tienes mediciones reales y dónde la línea sólo está uniendo.

c) Sale exactamente la misma gráfica que en a, aunque las filas estén revueltas. Esto sorprende a mucha gente: geom_line ordena los datos por el eje x antes de unirlos. Es decir, no une los puntos en el orden en que están en tu base, sino de izquierda a derecha.

d) Aquí sí sale un garabato. geom_path une los puntos en el orden exacto en que aparecen en la base, sin ordenarlos. Por eso la línea va y viene.

¿Y esto para qué me sirve? Para series de tiempo casi siempre quieres geom_line. geom_path sirve cuando el orden es la información: por ejemplo para dibujar la trayectoria de un huracán, el recorrido de una brigada, o una gráfica donde el eje x es la prevalencia y el y la mortalidad y quieres ver cómo se movió un país año con año.

4.5.3 Ejercicio: encuentra el error (trayectorias)

# a) Queremos una línea del número de casos por semana
ggplot(brote) +
  geom_line(aes(x = semana))
# b) Queremos la línea en rojo y más gruesa
ggplot(brote) +
  geom_line(aes(x = semana, y = casos, color = "red", size = 2))
# c) Queremos marcar la semana pico con una línea vertical
ggplot(brote) +
  geom_line(aes(x = semana, y = casos)) +
  geom_hline(xintercept = 3, linetype = "dashed")
# d) Queremos que el eje y empiece en cero
ggplot(brote) +
  geom_line(aes(x = semana, y = casos)) +
  ylim(0)
  1. Falta la y. Sin ella ggplot2 no sabe qué altura darle a la línea. Correcto: aes(x = semana, y = casos).

  2. color y size son valores fijos, así que van afuera del aes: geom_line(aes(x = semana, y = casos), color = "red", linewidth = 2). Como está escrito, te va a salir una línea de un color raro con dos leyendas absurdas. (Nota además que para líneas el argumento moderno es linewidth, no size: size sigue funcionando pero ggplot2 te avisa que ya está obsoleto para líneas.)

  3. geom_hline dibuja líneas horizontales y por eso su argumento es yintercept. Para una línea vertical en la semana 3 se usa geom_vline(xintercept = 3, linetype = "dashed").

  4. ylim necesita dos números, el mínimo y el máximo: ylim(0, 100). Si sólo quieres fijar el piso y que R decida el techo, usa expand_limits(y = 0) o ylim(0, NA).

    Tip

    Que el eje y empiece en cero no es un capricho: en gráficas de barras es obligatorio, porque el ojo compara alturas y un eje cortado exagera diferencias pequeñas. En gráficas de línea es discutible y a veces conviene acercarse a los datos, pero entonces hay que decirlo explícitamente en el pie de figura.

4.6 Ejercicio de cierre de la sección

Para este ejercicio puedes usar cualquier base(s) de datos que tú desees. La única petición es que no haya que limpiar los datos: si necesitas hacerlo por ahorita hazlo en Excel o elige una base distinta que no lo requiera.

El ejercicio consiste en realizar dos gráficas de distinto tipo (ej una de barras y una de tendencia) analizando elementos de tu base de datos.

Tip

Si no tienes datos para usar te recomiendo vacunacion_puebla_menores_1_anio.csv para una gráfica de barras (contiene la información de vacunas apliadas a menores de 1 año por jurisdicción en Puebla), polio-number-unvaccinated.csv para una gráfica de tendencias donde muestres cómo ha evolucionado la cantidad de niños en México que no recibieron vacuna contra la polio o bien OCDE_DTP_MEASLES_2008.csv para una gráfica de puntos donde cada punto es un país de la OCDE en 2008 y se grafican las tasas de vacunación con DTP y contra sarampión.

4.6.1 Antes de correr nada: dibújalas

Como parte del ejercicio de cierre, antes de escribir una sola línea de código haz el boceto de tus dos gráficas en una hoja: los ejes, más o menos las alturas, el título y qué quieres que la gente concluya al verla. Después escribe el código para llegar ahí.

Es el orden contrario al que todo el mundo usa (primero graficar y luego ver qué salió), y es el que sirve. Si no puedes dibujar la gráfica, es que todavía no sabes qué quieres decir con ella; y si no sabes qué quieres decir, ningún theme() te va a salvar.

Al final compara tu boceto con la gráfica que te quedó: ¿se parecen? Si no, ¿fue porque el código no hizo lo que pensabas o porque los datos no decían lo que suponías? Las dos respuestas son valiosas y son muy distintas.

4.6.2 Ejercicio de cierre con apoyo de una IA

Hazlo después de haber terminado tus dos gráficas por tu cuenta. El chiste es comparar, no ahorrarte el trabajo.

  1. Compite contra ella. Descríbele a una IA la misma gráfica que ya hiciste (las columnas de tu base, qué va en cada eje, qué quieres resaltar) y pídele el código de ggplot2. Corre su versión junto a la tuya. ¿Cuál se entiende mejor sin explicación? ¿Qué le copiarías y qué no?

  2. Revisión de accesibilidad. Pídele: “¿Los colores de este código de ggplot2 se distinguen para una persona con daltonismo? Si no, propón una paleta alternativa.”. Verifica su respuesta tú: instala colorBlindness y usa colorBlindness::cvdPlot(mi_grafica), o revisa la paleta en https://colorbrewer2.org marcando la casilla de colorblind safe. Éste es un caso donde vale la pena hacerle caso a la IA y comprobarlo, porque es un detalle que se olvida siempre.

  3. Que le busque errores a tu gráfica, no a tu código. Mándale tu gráfica terminada y pregúntale: “¿Qué podría malinterpretar alguien que vea esta gráfica sin contexto?”. De todo lo que te diga, quédate únicamente con lo que puedas verificar viendo tus propios datos.

  4. Lo que la IA no puede hacer. Contesta por escrito, en tres o cuatro renglones: de las decisiones que tomaste para tus dos gráficas (qué variable graficar, qué periodo, qué denominador, si el eje empieza en cero, qué se resalta y qué se deja en gris), ¿cuáles requieren saber de salud pública y no de R? Ésas son exactamente las que no puedes delegarle a nadie, ni a una IA ni a quien te ayude con el código.

4.7 Para saber más

El libro de ggplot2 es una excelente referencia para la parte de graficación así como las notas en español de rafalab. Por otro lado el libro de Fundamentals of Data Visualization analiza más a fondo cómo presentar datos de formas que convenzan al público. Para armar historias a partir de los datos ve el libro de Story-telling with data

4.8 Continuación

¡Felicidades! Ya terminaste esta sección. Puedes ir a:

4.9 Sistema

sessioninfo::session_info()
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.5.3 (2026-03-11)
 os       macOS Sequoia 15.7.3
 system   x86_64, darwin20
 ui       X11
 language (EN)
 collate  es_ES.UTF-8
 ctype    es_ES.UTF-8
 tz       America/Mexico_City
 date     2026-08-05
 pandoc   3.10.1 @ /usr/local/bin/ (via rmarkdown)
 quarto   1.10.18 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package           * version date (UTC) lib source
 bit                 4.6.0   2025-03-06 [1] CRAN (R 4.5.0)
 bit64               4.8.2   2026-05-19 [1] CRAN (R 4.5.2)
 cli                 3.6.6   2026-04-09 [1] CRAN (R 4.5.2)
 codetools           0.2-20  2024-03-31 [1] CRAN (R 4.5.3)
 cowplot           * 1.2.0   2025-07-07 [1] CRAN (R 4.5.1)
 crayon              1.5.3   2024-06-20 [1] CRAN (R 4.5.0)
 digest              0.6.39  2025-11-19 [1] CRAN (R 4.5.1)
 dplyr             * 1.2.1   2026-04-03 [1] CRAN (R 4.5.2)
 evaluate            1.0.5   2025-08-27 [1] CRAN (R 4.5.1)
 farver              2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
 fastmap             1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
 fontBitstreamVera   0.1.1   2017-02-01 [1] CRAN (R 4.5.0)
 fontLiberation      0.1.0   2016-10-15 [1] CRAN (R 4.5.0)
 fontquiver          0.2.1   2017-02-01 [1] CRAN (R 4.5.0)
 forcats           * 1.0.1   2025-09-25 [1] CRAN (R 4.5.1)
 gdtools             0.5.0   2026-02-09 [1] CRAN (R 4.5.1)
 generics            0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
 ggformula         * 1.0.1   2026-01-17 [1] CRAN (R 4.5.1)
 ggiraph           * 0.9.6   2026-02-21 [1] CRAN (R 4.5.2)
 ggplot2           * 4.0.3   2026-04-22 [1] CRAN (R 4.5.2)
 ggrepel           * 0.9.8   2026-03-17 [1] CRAN (R 4.5.2)
 ggridges          * 0.5.7   2025-08-27 [1] CRAN (R 4.5.1)
 glue                1.8.1   2026-04-17 [1] CRAN (R 4.5.2)
 gridExtra         * 2.3     2017-09-09 [1] CRAN (R 4.5.0)
 gtable              0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
 haven               2.5.5   2025-05-30 [1] CRAN (R 4.5.0)
 hms                 1.1.4   2025-10-17 [1] CRAN (R 4.5.1)
 htmltools           0.5.9   2025-12-04 [1] CRAN (R 4.5.1)
 htmlwidgets         1.6.4   2023-12-06 [1] CRAN (R 4.5.0)
 jsonlite            2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
 kableExtra        * 1.4.0   2024-01-24 [1] CRAN (R 4.5.0)
 knitr               1.51    2025-12-20 [1] CRAN (R 4.5.1)
 labeling            0.4.3   2023-08-29 [1] CRAN (R 4.5.0)
 labelled            2.16.0  2025-10-22 [1] CRAN (R 4.5.1)
 lifecycle           1.0.5   2026-01-08 [1] CRAN (R 4.5.1)
 lubridate         * 1.9.5   2026-02-04 [1] CRAN (R 4.5.1)
 magrittr            2.0.5   2026-04-04 [1] CRAN (R 4.5.2)
 MASS                7.3-65  2025-02-28 [1] CRAN (R 4.5.3)
 mosaicCore          0.9.5   2025-07-30 [1] CRAN (R 4.5.1)
 otel                0.2.0   2025-08-29 [1] CRAN (R 4.5.1)
 pillar              1.11.1  2025-09-17 [1] CRAN (R 4.5.1)
 pkgconfig           2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
 png                 0.1-9   2026-03-15 [1] CRAN (R 4.5.2)
 purrr             * 1.2.2   2026-04-10 [1] CRAN (R 4.5.2)
 R6                  2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
 RColorBrewer        1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
 Rcpp                1.1.2   2026-07-05 [1] CRAN (R 4.5.2)
 readr             * 2.2.0   2026-02-19 [1] CRAN (R 4.5.2)
 rlang               1.3.0   2026-07-05 [1] CRAN (R 4.5.2)
 rmarkdown           2.31    2026-03-26 [1] CRAN (R 4.5.2)
 rstudioapi          0.19.0  2026-06-11 [1] CRAN (R 4.5.2)
 S7                  0.2.2   2026-04-22 [1] CRAN (R 4.5.2)
 scales            * 1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
 sessioninfo         1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
 stringi             1.8.7   2025-03-27 [1] CRAN (R 4.5.0)
 stringr           * 1.6.0   2025-11-04 [1] CRAN (R 4.5.1)
 svglite             2.2.2   2025-10-21 [1] CRAN (R 4.5.1)
 systemfonts         1.3.2   2026-03-05 [1] CRAN (R 4.5.2)
 textshaping         1.0.5   2026-03-06 [1] CRAN (R 4.5.2)
 tibble            * 3.3.1   2026-01-11 [1] CRAN (R 4.5.1)
 tidyr             * 1.3.2   2025-12-19 [1] CRAN (R 4.5.1)
 tidyselect          1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
 tidyverse         * 2.0.0   2023-02-22 [1] CRAN (R 4.5.0)
 timechange          0.4.0   2026-01-29 [1] CRAN (R 4.5.1)
 tzdb                0.5.0   2025-03-15 [1] CRAN (R 4.5.0)
 vctrs               0.7.3   2026-04-11 [1] CRAN (R 4.5.2)
 viridisLite         0.4.3   2026-02-04 [1] CRAN (R 4.5.1)
 vroom               1.7.1   2026-03-31 [1] CRAN (R 4.5.2)
 withr               3.0.3   2026-06-19 [1] CRAN (R 4.5.2)
 xfun                0.60    2026-07-09 [1] CRAN (R 4.5.2)
 xml2                1.6.0   2026-06-22 [1] CRAN (R 4.5.2)
 yaml                2.3.12  2025-12-10 [1] CRAN (R 4.5.1)

 [1] /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────