Visualización de datos en R con ggplot2
Aprende a leer un JSON, convertirlo en un data frame y construir gráficos profesionales con ggplot2, paso a paso y con explicaciones claras. Pensado para personas que recién parten en programación.
Todo el curso usa un mismo dataset real y entrañable: 14 años de la vida del gato Pépito 🐱, que tuitea cada vez que entra o sale de casa desde 2011. Datos de verdad, con sus rarezas del mundo real.
Este curso es un complemento de Hazla con Datos. Puedes tomarlo antes, durante o después del curso principal.
Conoce el dataset: el gato Pépito
Durante todo el curso usaremos un mismo archivo JSON público con cada entrada y salida de Pépito desde noviembre de 2011. No hay que descargar nada: lo leeremos directo desde la web con jsonlite::fromJSON(). Tiene los típicos "problemas reales" que vas a aprender a resolver: fechas en texto, zonas horarias y eventos sueltos.
Sitio oficial de Pépito: pepitothecat.xyz (incluye una sección para desarrolladores).
Prerequisito: Configurar tu entorno
Para seguir los ejemplos necesitas tener R y Positron instalados. Si recién partes, sigue primero la guía Configuración del Entorno: te lleva paso a paso de cero (terminal, R con Rig, Pak, Positron y más).
Ir a Configuración del Entorno →Parte de Hazla con Datos
Este curso es un recurso complementario de Hazla con Datos, una comunidad enfocada en programación y ciencia de datos en salud. Encuentra más cursos, recursos y herramientas para tu camino profesional.
Visitar hazlacondatos.com →Bienvenida: 14 años de un gato y un montón de datos
Bienvenida
Antes de escribir una sola línea de código, déjame contarte una historia.
En 2011, un ingeniero francés llamado Clément Storck le puso un sensor a la gatera (la puertita por donde entra y sale su gato) de su casa. Cada vez que su gato negro, Pépito, entra o sale, el sensor lo detecta, saca una foto y publica un tuit automático: “Pépito est sorti” (Pépito salió) o “Pépito est rentré” (Pépito volvió). Lleva haciéndolo sin parar desde noviembre de 2011.
El resultado es uno de los datasets más entrañables de internet: casi 14 años de la vida cotidiana de un gato, evento por evento. ¿A qué hora sale? ¿Cuánto tiempo pasa afuera? ¿Sale más en verano? ¿Cambió su rutina con los años? Todo eso está en los datos, esperando a que alguien lo grafique.
Ese alguien vas a ser tú. Y la herramienta va a ser ggplot2.
Vamos a tomar un archivo de datos crudo de la web y, paso a paso, convertirlo en gráficos claros y bonitos que cuenten la historia de Pépito.
¿Qué es ggplot2?
ggplot2 es el paquete de R para hacer gráficos. Es, probablemente, la razón número uno por la que mucha gente que trabaja con datos elige R. Con él puedes hacer desde un gráfico de barras rápido hasta una visualización lista para publicar en un informe o una revista científica.
Lo que lo hace especial es que no aprendes 50 funciones distintas (una para barras, otra para líneas, otra para histogramas…). En vez de eso, aprendes un único sistema —una “gramática”— y con ella construyes cualquier gráfico combinando piezas. Por eso se llama ggplot: la gg viene de Grammar of Graphics (la gramática de los gráficos).
¿Gramática? ¿Como la del lenguaje?
Exactamente. Así como en español combinas sujeto + verbo + complemento para formar cualquier frase, en ggplot2 combinas datos + una forma geométrica + unas reglas visuales para formar cualquier gráfico. Una vez que entiendes las piezas, las mezclas como quieras. Eso lo veremos en detalle en el módulo 5.
Lo que vas a construir
Esta es la gráfica final del curso. Resume 14 años en un solo cuadro: cada celda es un mes, el color indica cuántos minutos —en mediana— pasaba Pépito fuera de casa.
El gráfico estrella
Un mapa de calor (heatmap) de año × mes, donde el color va de morado oscuro (poco rato fuera) a amarillo (mucho rato fuera), con el número escrito dentro de cada celda. Se ve, por ejemplo, que en julio de 2015 Pépito batió su récord: una mediana de 252 minutos fuera de casa. ¡Más de 4 horas de aventura diaria!
No te preocupes si ahora suena complicado. Cuando lleguemos al módulo 11 lo vas a construir tú mismo, capa por capa, y vas a entender cada línea.
Pero no vamos a empezar por ahí. Vamos a empezar por lo básico (barras simples) y a subir de a poco. En el camino harás muchos gráficos distintos: barras, líneas, histogramas, densidades, cajas (boxplots), líneas de tendencia y, finalmente, mapas de calor.
El camino completo
Hacer una visualización casi nunca es “escribir el gráfico y listo”. Hay un flujo de trabajo que se repite en todos los proyectos de datos, y este curso lo recorre entero:
El flujo que vamos a seguir
- Traer los datos a R (en nuestro caso, leer un archivo JSON desde la web).
- Entender qué tenemos: cuántas filas, qué columnas, de qué tipo.
- Preparar los datos: arreglar las fechas, crear columnas nuevas (la hora, el día, el año…).
- Graficar: empezar simple y agregar capas hasta que el gráfico diga lo que queremos.
- Pulir y compartir: títulos, colores, temas y guardar la imagen.
Los módulos 3 y 4 cubren traer y preparar los datos. Del 5 al 11 graficamos. El 12 es para pulir y exportar. No te saltes los módulos 3 y 4: el 80% del trabajo real de visualización es, en realidad, preparar bien los datos. Un gráfico bonito sobre datos mal preparados es un gráfico equivocado.
Qué vas a saber al terminar
Al final del curso vas a poder, sin pánico:
- Leer un archivo JSON desde una URL y convertirlo en una tabla (
data frame) de R. - Transformar fechas que vienen como texto (
"Sun Nov 13 10:47:15 +0000 2011") en fechas reales con las que se puede calcular. - Crear columnas nuevas: la hora del día, el día de la semana, el año, la estación.
- Entender la gramática de ggplot2 y construir un gráfico capa por capa.
- Elegir el tipo de gráfico correcto según la pregunta: barras, líneas, histogramas, boxplots, mapas de calor.
- Cambiar colores, escalas, títulos y temas, y guardar tu gráfico como imagen para un informe.
Detente y piensa
Antes de avanzar, piensa: si tuvieras los datos de Pépito frente a ti, ¿qué le preguntarías? ¿A qué hora sale más? ¿Sale más los fines de semana? ¿Pasa más rato fuera en verano? Anota una o dos preguntas. A lo largo del curso vas a poder responderlas tú mismo con un gráfico.
Cómo está organizado el curso
| Bloque | Módulos | Qué aprendes |
|---|---|---|
| Empezar | 1 – 2 | La idea de ggplot2 y dejar el entorno listo. |
| De JSON a datos | 3 – 4 | Traer el JSON a R y preparar las fechas y columnas. |
| ggplot2 desde cero | 5 – 9 | La gramática, barras, líneas, distribuciones y boxplots. |
| Análisis y cierre | 10 – 12 | Tendencias, el mapa de calor final y cómo pulir y exportar. |
Cada módulo tiene explicación paso a paso, código comentado línea por línea, errores comunes (los que de verdad te van a pasar) y un ejercicio con solución colapsable. Lee, prueba el código en tu propio R, equivócate un par de veces, lee la sección de errores y vuelve al ejercicio. Así se aprende de verdad.
La regla de oro para aprender a programar
No copies y pegues sin más. Escribe el código a mano, ejecútalo, y cuando algo falle (va a fallar, a todos nos pasa), lee el mensaje de error con calma. Programar es, en gran parte, aprender a leer errores. En este curso te vamos a mostrar los errores típicos a propósito para que cuando te aparezcan ya sepas qué hacer.
Una nota sobre el pipe |>
En todo el curso vas a ver mucho este símbolo: |>. Se llama pipe nativo (tubería) y es una de las cosas más útiles de R moderno. Por ahora solo quédate con la idea: sirve para encadenar pasos, como decir “toma esto, y luego hazle esto, y luego esto otro”. Lo explicaremos cuando lo usemos por primera vez. Es parte de R desde la versión 4.1 (2021) y no necesita ningún paquete extra.
Listo para empezar
En el próximo módulo dejamos el entorno listo: instalamos los paquetes que vamos a usar (jsonlite, dplyr, lubridate, ggplot2) y aprendemos la diferencia entre instalar y cargar un paquete, que es la confusión clásica al partir.
Ejercicio 1 — Observa el gato
No hay código todavía. Solo dos cosas:
- Entra al sitio oficial de Pépito, pepitothecat.xyz (o busca PepitoTheCat en redes), y mira algunos de sus posts. Fíjate en el formato: cada uno dice si entró o salió, con una hora. El sitio incluso tiene una sección para desarrolladores.
- Anota tres preguntas que te gustaría responder con estos datos. Por ejemplo: “¿A qué hora sale más?” o “¿Pasa más rato fuera los fines de semana?”.
Guarda esas tres preguntas. En el módulo 11 vas a poder responderlas mirando tus propios gráficos.
Ver ejemplo de preguntas
Ejemplo de tres buenas preguntas:
- ¿En qué franja horaria del día Pépito sale más? (la responderemos con un gráfico de barras en el módulo 6).
- ¿Cuánto tiempo pasa normalmente fuera de casa? (un histograma, módulo 8).
- ¿Ha cambiado su rutina a lo largo de los años? (una línea de tendencia y un mapa de calor, módulos 10 y 11).
Lo bonito de tener un dataset real y largo es que casi cualquier pregunta razonable tiene respuesta. La habilidad que vas a desarrollar es traducir una pregunta en un gráfico.
Preparar el entorno: R, Positron y los paquetes
El objetivo del módulo
Antes de graficar necesitamos dos cosas: R funcionando y los paquetes del curso instalados. En este módulo dejamos todo listo y, de paso, aclaramos una confusión clásica de quien recién parte: la diferencia entre instalar y cargar un paquete.
Prerequisito: configura tu entorno primero
Este curso asume que ya tienes R y Positron (el editor que recomendamos) instalados y funcionando. Si todavía no, detente aquí y sigue primero esta guía paso a paso, hecha para principiantes absolutos:
Te lleva de cero: terminal, instalar R con Rig, pak, Positron y todo lo necesario. Cuando termines, vuelve aquí.
Instalar vs. cargar: la confusión número uno
Esto confunde a casi todo el mundo al empezar, así que vamos con calma usando una analogía:
- Instalar un paquete es como comprar un libro y ponerlo en tu estante. Lo haces una sola vez. El libro se queda en tu computador para siempre (hasta que lo actualices o borres).
- Cargar un paquete es como bajar el libro del estante y abrirlo sobre la mesa. Lo haces cada vez que abres R y vas a usar ese paquete.
Si instalas pero no cargas, R te dirá “no encuentro esa función”. Si intentas cargar algo que no instalaste, R te dirá “no existe ese paquete”. Son dos pasos distintos.
En código
- Instalar (una vez):
pak::pak("ggplot2") - Cargar (cada sesión):
library(ggplot2)
Paso 1 — Instalar los paquetes del curso
Vamos a usar cuatro paquetes principales. Los instalamos todos de una vez. Abre R (o Positron) y ejecuta:
# Instalar (esto se hace UNA sola vez en tu computador)pak::pak(c("jsonlite", "lubridate", "dplyr", "ggplot2"))¿Qué hace cada uno?
jsonlite— lee archivos JSON (el formato en que vienen los datos de Pépito) y los convierte en tablas de R.lubridate— trabaja con fechas y horas sin volverse loco. Lo necesitamos porque las fechas de Pépito vienen como texto.dplyr— manipula tablas: filtrar filas, crear columnas, agrupar, resumir. Es la navaja suiza de los datos en R.ggplot2— la estrella del curso: hace los gráficos.
Atajo: ¿ya tienes `tidyverse`?
ggplot2, dplyr y lubridate forman parte del tidyverse, una colección de paquetes que trabajan muy bien juntos. Si prefieres, puedes instalar todo el conjunto de una vez con:
pak::pak("tidyverse")Así tendrás esos tres (y muchos más, como tidyr o readr) sin instalarlos por separado. Desde tidyverse 2.0.0 (2023), lubridate es parte del core, así que library(tidyverse) carga de una vez ggplot2, dplyr y lubridate. Ojo: jsonlite no viene dentro del tidyverse, así que si instalas el conjunto, agrégalo aparte:
pak::pak(c("tidyverse", "jsonlite"))En este curso cargamos cada paquete por separado (library(ggplot2), library(dplyr)…) para que veas exactamente de qué paquete viene cada función. Si usas library(tidyverse), esos tres ya quedan cargados y solo te faltaría library(jsonlite).
¿Por qué `pak`?
Quizás en otros tutoriales viste otra forma de instalar paquetes. En este curso usamos siempre pak, porque es más moderno: es más rápido, te explica mejor los errores y resuelve solo las dependencias del sistema. La instalación de pak ya viene cubierta en la guía de Configuración del Entorno que hiciste como prerequisito, así que deberías tenerlo listo.
Paso 2 — Cargar los paquetes
Cada vez que empieces una sesión de R para trabajar en el curso, carga los paquetes al inicio de tu script con library():
# Cargar (esto va al principio de tu script, CADA sesión)library(jsonlite)library(lubridate)library(dplyr)library(ggplot2)Pon los `library()` siempre arriba
La costumbre profesional es poner todos los library() juntos en las primeras líneas de tu script. Así, cualquiera que lo abra (incluido tú dentro de seis meses) ve de inmediato qué paquetes necesita. No los escondas en medio del código.
Paso 3 — Comprobar que todo funciona
Hagamos una prueba mínima para confirmar que los paquetes se cargaron bien. Este código dibuja un gráfico de ejemplo con datos que ya vienen dentro de R:
library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point()Si te aparece una ventana (o un panel) con una nube de puntos, ¡felicitaciones! ggplot2 está funcionando. No importa qué significa el gráfico todavía; solo queríamos confirmar que el motor enciende.
Error típico: `could not find function 'ggplot'`
Causa: instalaste el paquete pero olvidaste cargarlo con library(ggplot2) en esta sesión. Recuerda la analogía: el libro está en el estante, pero no lo abriste sobre la mesa.
Solución: ejecuta library(ggplot2) y vuelve a correr el código.
Error típico: `there is no package called 'ggplot2'`
Causa: el paquete no está instalado (o se instaló a medias).
Solución: ejecuta pak::pak("ggplot2") y espera a que termine sin errores. Luego library(ggplot2).
Un par de tips antes de seguir
Reinicia R cuando algo se ponga raro
Si llevas un rato y empiezan a pasar cosas extrañas (resultados que no calzan, errores sin sentido), reinicia la sesión de R: en Positron, Session → Restart R. Esto borra todas las variables y te deja “en limpio”. Sorprende cuántos problemas se arreglan así.
Crea un proyecto para el curso
Crea una carpeta nueva (por ejemplo, curso-ggplot2) y ábrela en Positron con File → Open Folder. Guarda ahí tu script .R. Tener un proyecto ordenado desde el principio te va a ahorrar dolores de cabeza con las rutas más adelante.
Ejercicio
Ejercicio 2 — Deja tu entorno listo
- Instala los cuatro paquetes del curso con un solo
pak::pak(...). - Crea un script nuevo llamado
pepito.Ry escribe en las primeras líneas los cuatrolibrary(). - Ejecuta el gráfico de prueba con
mtcars. - Confirma que ves la nube de puntos.
Si todo funciona, estás listo para traer los datos de Pépito en el próximo módulo.
Ver solución
# 1. Instalar (una sola vez)pak::pak(c("jsonlite", "lubridate", "dplyr", "ggplot2"))
# 2. Al inicio de pepito.R, cargar los paqueteslibrary(jsonlite)library(lubridate)library(dplyr)library(ggplot2)
# 3. Gráfico de pruebaggplot(mtcars, aes(x = wt, y = mpg)) + geom_point()Si ves los puntos, tu entorno está perfecto. Si no, vuelve a leer los dos errores típicos de más arriba: el 95% de las veces es que falta library() o que el pak::pak() no terminó bien.
Nota: mtcars es un conjunto de datos de ejemplo que viene dentro de R (datos de autos). Lo usamos solo para probar; a partir del próximo módulo trabajamos con los datos de Pépito.
De la web a R: leer un JSON
El objetivo del módulo
Vamos a traer los datos de Pépito desde la web hasta una variable en R que podamos mirar y manipular. Los datos están en un archivo JSON público, así que primero entendamos qué es eso.
¿Qué es un JSON?
JSON (se lee “yeison”) es un formato de texto muy común para guardar e intercambiar datos, sobre todo en internet. Casi todas las APIs (los “enchufes” por los que las aplicaciones se pasan datos) hablan en JSON.
Un JSON se ve así:
[ { "full_text": "Pepito est sorti (12:47:41)", "way": "out", "created_at": "Sun Nov 13 10:47:15 +0000 2011", "media": "" }, { "full_text": "Pepito est rentré (9:02:53)", "way": "in", "created_at": "Mon Nov 14 07:02:25 +0000 2011", "media": "" }]Léelo con calma, porque es muy intuitivo:
- Los corchetes
[ ]envuelven una lista de cosas. Aquí, una lista de eventos. - Las llaves
{ }envuelven un evento, con sus datos. - Dentro, cada dato es un par
"nombre": valor. Por ejemplo,"way": "out"significa que ese campo se llamawayy su valor es"out".
Cada { } de la lista es un evento de Pépito. Y hay casi 14 años de ellos.
Los campos de los datos de Pépito
full_text— el texto del tuit, en francés."sorti"= salió,"rentré"= volvió.way— la dirección del movimiento:"out"(salió) o"in"(entró). Este campo es oro: con él sabremos cuándo sale y cuándo vuelve.created_at— la fecha y hora del evento, como texto, en formato de Twitter y en hora UTC.media— la URL de la foto del evento. En los primeros años suele venir vacío (la cámara llegó después), pero en los años más recientes trae la imagen en muchos eventos. No la usaremos en este curso.
Leer el JSON con una sola línea
La función fromJSON() del paquete jsonlite hace la magia: le pasas la dirección (URL) del archivo y te devuelve una tabla de R (un data frame) lista para usar.
library(jsonlite)
url <- "https://raw.githubusercontent.com/clement87/Pepito-data/refs/heads/main/tweets.json"
pepito_df <- fromJSON(url)Línea por línea:
library(jsonlite)— carga el paquete que sabe leer JSON.url <- "https://..."— guardamos la dirección del archivo en una variable llamadaurl. La flechita<-significa “asigna a la izquierda lo de la derecha”. Guardar la URL en una variable hace el código más limpio.pepito_df <- fromJSON(url)— leemos el archivo de esa URL y guardamos el resultado en una variable llamadapepito_df(df por data frame, que es como R llama a las tablas).
¿Qué es un data frame?
Un data frame es simplemente una tabla: tiene filas y columnas, como una hoja de Excel. Cada fila es un evento de Pépito; cada columna es un dato (full_text, way, created_at, media). Es la estructura más importante de R para trabajar con datos, y ggplot2 siempre grafica a partir de un data frame.
Mirar qué llegó
Regla de oro: después de leer datos, siempre míralos antes de hacer nada más. Hay tres funciones que usarás siempre para esto:
# Las primeras 6 filashead(pepito_df)
# El número de filas y columnasdim(pepito_df)
# Un resumen de columnas, tipos y primeros valores (necesita dplyr)library(dplyr)glimpse(pepito_df)glimpse() es la más útil. Te muestra algo así:
Rows: 80,000+Columns: 4$ full_text <chr> "Pepito est sorti (12:47:41)", "Pepito est sorti...$ way <chr> "out", "out", "out", "out", "in", "out", ...$ created_at <chr> "Sun Nov 13 10:47:15 +0000 2011", "Sun Nov 13 ...$ media <chr> "", "", "", "", "", ...Léelo así: hay muchísimas filas (son años de datos) y 4 columnas, todas de tipo <chr> (texto, de character). Fíjate en un detalle clave: created_at es texto, no una fecha de verdad. R todavía no sabe que "Sun Nov 13 10:47:15 +0000 2011" es una fecha; para él es solo una palabra larga. Eso lo arreglamos en el módulo 4.
Detente y piensa
¿Por qué importa que created_at sea texto y no fecha? Porque con texto no puedes calcular. No puedes preguntar “¿cuántos minutos pasaron entre que salió y volvió?” si R cree que las fechas son simples palabras. Convertir ese texto en fecha real es el paso que desbloquea todo el análisis.
Contar entradas y salidas
Hagamos nuestra primera pregunta a los datos. ¿Cuántas veces salió y cuántas entró? La función count() de dplyr cuenta cuántas filas hay de cada valor:
library(dplyr)
pepito_df |> count(way)Aquí aparece por primera vez el pipe |>. Léelo como la palabra “luego”: “toma pepito_df, luego cuéntalo por way”. El resultado es algo como:
way n <chr> <int>1 in 400002 out 40000Tiene sentido: para volver a casa, primero hay que salir, así que las entradas y salidas son más o menos iguales.
El pipe `|>` en una frase
x |> f() es lo mismo que f(x). El pipe toma lo que está a su izquierda y lo mete como primer argumento de la función de la derecha. Su gracia es que permite encadenar pasos de forma que se lean de arriba a abajo, como una receta. Lo usaremos sin parar.
Errores comunes al leer un JSON
Error 1: `could not find function 'fromJSON'`
Causa: no cargaste jsonlite.
Solución: agrega library(jsonlite) antes de llamar a fromJSON().
Error 2: `Could not resolve host` o el código se queda pegado
Causa: fromJSON(url) descarga el archivo desde internet. Si no tienes conexión (o estás detrás de un firewall corporativo muy estricto), falla.
Solución: confirma que tienes internet. Si la red de tu trabajo bloquea descargas, prueba desde otra red. El archivo es público, así que cualquier navegador puede abrir la URL para comprobar que existe.
Error 3: escribir mal la URL
Una URL larga es fácil de copiar a medias. Si te falta un pedazo, R no encuentra el archivo. Solución: copia y pega la URL completa, sin espacios ni saltos de línea en medio.
Lee una vez, trabaja muchas
Descargar el JSON toma unos segundos. No pongas fromJSON(url) dentro de cada gráfico. Léelo una sola vez al principio de tu script, guárdalo en pepito_df, y de ahí en adelante trabaja siempre con esa variable. Tu código será mucho más rápido.
Ejercicio
Ejercicio 3 — Trae y explora los datos
- Carga
jsonliteydplyr. - Lee el JSON de Pépito en una variable
pepito_df. - Usa
glimpse(pepito_df)y responde: ¿cuántas columnas hay? ¿de qué tipo escreated_at? - Usa
count(way)y mira cuántas salidas y entradas hay. - Extra: ejecuta
head(pepito_df$full_text)para ver los textos. ¿En qué idioma están?
Ver solución
library(jsonlite)library(dplyr)
url <- "https://raw.githubusercontent.com/clement87/Pepito-data/refs/heads/main/tweets.json"pepito_df <- fromJSON(url)
glimpse(pepito_df)pepito_df |> count(way)head(pepito_df$full_text)Respuestas:
- Hay 4 columnas (
full_text,way,created_at,media), todas de tipo texto (<chr>). created_ates texto, no fecha. Ese es justo el problema que resolveremos en el módulo 4.- Los textos están en francés (
"sorti","rentré"), porque Pépito vive en Francia. No necesitamos entender francés: nos basta con la columnaway(in/out).
El símbolo $ (en pepito_df$full_text) sirve para sacar una columna específica de la tabla. Lo verás mucho: tabla$columna.
Fechas y columnas nuevas con lubridate
El objetivo del módulo
Tenemos los datos, pero la columna de fecha (created_at) es texto, y con texto no se puede calcular ni graficar bien. En este módulo la convertimos en una fecha-hora de verdad y, a partir de ella, creamos columnas nuevas que vamos a usar en casi todos los gráficos: la hora del día, la fecha y el día de la semana.
Este es el módulo más “técnico” de la parte de datos, pero también el más importante. Tómalo con calma.
El problema: una fecha que es texto
Recordemos cómo se ve created_at:
"Sun Nov 13 10:47:15 +0000 2011"Para un humano es claro: domingo 13 de noviembre de 2011, a las 10:47:15, en hora UTC. Pero para R es una palabra larga sin significado. Si intentas restar dos de estas “palabras” para ver cuánto tiempo pasó, R se queja. Necesitamos traducir ese texto al idioma de fechas de R.
Convertir texto en fecha con parse_date_time()
La herramienta es parse_date_time(), del paquete lubridate. Le decimos en qué orden vienen las piezas de la fecha y él hace la conversión:
library(lubridate)
pepito_df$datetime <- parse_date_time( pepito_df$created_at, orders = "a b d H:M:S z Y", tz = "UTC", locale = "C")Vamos por partes, porque cada argumento importa:
pepito_df$created_at— la columna de texto que queremos convertir.orders = "a b d H:M:S z Y"— el “molde” que describe el orden de las piezas en el texto. Cada letra es un pedazo:a= nombre del día abreviado (Sun)b= nombre del mes abreviado (Nov)d= día del mes (13)H:M:S= hora:minuto:segundo (10:47:15)z= la zona horaria (+0000)Y= año con 4 dígitos (2011)
tz = "UTC"— le decimos que estas horas están en UTC (la hora de referencia mundial), porque así las guarda Twitter.locale = "C"— ¡clave! Le decimos que los nombres del día y del mes están en inglés (Sun,Nov). Sin esto, si tu computador está en español, R buscaría"Dom"y"Nov"en español y fallaría.
El resultado es una columna nueva, datetime, que R sí entiende como fecha-hora.
El error más común aquí: ¡todo sale NA!
Si después de convertir ves que la columna datetime está llena de NA (valores vacíos), casi siempre es por una de estas dos razones:
- Olvidaste
locale = "C"y tu sistema está en español: R no reconoce"Sun"ni"Nov". - El
ordersno calza con el formato real del texto.
Solución: revisa un valor crudo con pepito_df$created_at[1] y compáralo letra por letra con tu orders. El 99% de los problemas de fechas son un orders o un locale mal puestos.
El detalle de la zona horaria: pasar a hora de Francia
Los datos están en UTC, pero Pépito vive en Francia. Si queremos saber “¿a qué hora sale Pépito?”, nos interesa la hora local francesa, no la UTC. En verano Francia va 2 horas adelante de UTC; en invierno, 1 hora. with_tz() hace esa conversión automáticamente, respetando incluso el horario de verano:
pepito_df$datetime_fr <- with_tz(pepito_df$datetime, tzone = "Europe/Paris")Creamos una columna nueva, datetime_fr, que es el mismo instante pero expresado en hora de París.
`with_tz()` vs `force_tz()` — no las confundas
with_tz()= “muéstrame este mismo instante, pero en otra zona horaria”. El momento real no cambia, solo cómo lo leemos. Es la que queremos.force_tz()= “cambia la etiqueta de zona sin convertir la hora”. Esto sí cambia el instante real. Úsala solo si los datos vinieron con la zona equivocada de origen.
Para Pépito usamos with_tz(): las horas están bien en UTC, solo queremos verlas en hora francesa.
Crear columnas útiles: fecha, hora y día de la semana
Ahora que tenemos una fecha-hora real (datetime_fr), podemos extraer pedazos de ella. Cada uno será una columna nueva que usaremos para graficar:
# Solo la fecha (sin la hora): 2011-11-13pepito_df$date <- as_date(pepito_df$datetime_fr)
# Solo la hora del día como número 0–23pepito_df$hour <- hour(pepito_df$datetime_fr)
# El día de la semana, con etiqueta y empezando en lunespepito_df$weekday <- wday(pepito_df$datetime_fr, label = TRUE, week_start = 1)Qué hace cada función de lubridate:
as_date()— se queda solo con el día (sirve para gráficos por fecha).hour()— extrae la hora como número entero de 0 a 23 (sirve para “¿a qué hora sale?”).wday(..., label = TRUE, week_start = 1)— el día de la semana.label = TRUElo devuelve como nombre (lun,mar…) en vez de número;week_start = 1hace que la semana empiece en lunes (en R, por defecto, empieza en domingo).
Revisar el resultado
Como siempre después de transformar: mira lo que quedó.
library(dplyr)glimpse(pepito_df)Ahora deberías ver las columnas nuevas con los tipos correctos:
$ datetime <dttm> 2011-11-13 10:47:15, ...$ datetime_fr <dttm> 2011-11-13 11:47:15, ... (¡una hora más: invierno en Francia!)$ date <date> 2011-11-13, ...$ hour <int> 11, ...$ weekday <ord> dom, dom, dom, ...<dttm> significa fecha-hora; <date> fecha; <int> número entero; <ord> factor ordenado (los días de la semana tienen orden). Si ves estos tipos, lo lograste: los datos ya están listos para graficar.
Versión con mutate() (más ordenada)
Ir creando columnas con pepito_df$x <- ... funciona perfecto. Pero a medida que avances verás esta forma encadenada con mutate() de dplyr, que hace lo mismo en un solo bloque:
pepito_df <- pepito_df |> mutate( datetime = parse_date_time(created_at, "a b d H:M:S z Y", tz = "UTC", locale = "C"), datetime_fr = with_tz(datetime, "Europe/Paris"), date = as_date(datetime_fr), hour = hour(datetime_fr), weekday = wday(datetime_fr, label = TRUE, week_start = 1) )Las dos formas dan el mismo resultado. Usa la que te resulte más clara; mutate() se vuelve más cómoda cuando creas muchas columnas.
Dos estilos, un mismo resultado
No hay una forma “correcta”: el estilo pepito_df$x <- ... es más explícito al empezar, y mutate() es más compacto y encadenable. En el curso verás ambos; quédate con el que leas con más comodidad.
Errores comunes con fechas
Error: las horas se ven 1–2 horas corridas
Si las horas no calzan con lo que esperas, probablemente mezclaste datetime (UTC) con datetime_fr (París) en algún gráfico. Sé consistente: para todo lo que sea “hora local de Pépito”, usa siempre datetime_fr y las columnas derivadas de ella (hour, date, weekday).
Error: `hour` te sale como texto y no como número
Si extraes la hora desde la columna de texto original en vez de desde datetime_fr, no funcionará. Asegúrate de que hour() reciba la columna ya convertida (datetime_fr), no created_at.
Detente y piensa
Fíjate en algo bonito: con with_tz(), el primer evento (13 nov 2011) suma +1 hora (invierno), pero un evento de julio sumaría +2 horas (verano). lubridate conoce las reglas del horario de verano de cada país y las aplica solo. Hacer esto a mano sería una pesadilla; por eso usamos un paquete de fechas en vez de improvisar.
Ejercicio
Ejercicio 4 — Prepara las columnas de fecha
Partiendo del pepito_df que leíste en el módulo 3:
- Crea la columna
datetimeconvirtiendocreated_atconparse_date_time()(¡no olvideslocale = "C"!). - Crea
datetime_frpasando a hora de París conwith_tz(). - Crea
hour,dateyweekday. - Ejecuta
glimpse(pepito_df)y confirma quehoures<int>ydatees<date>. - Extra: ejecuta
pepito_df |> count(weekday). ¿Algún día de la semana se ve más activo?
Ver solución
library(lubridate)library(dplyr)
pepito_df$datetime <- parse_date_time(pepito_df$created_at, orders = "a b d H:M:S z Y", tz = "UTC", locale = "C")pepito_df$datetime_fr <- with_tz(pepito_df$datetime, tzone = "Europe/Paris")pepito_df$date <- as_date(pepito_df$datetime_fr)pepito_df$hour <- hour(pepito_df$datetime_fr)pepito_df$weekday <- wday(pepito_df$datetime_fr, label = TRUE, week_start = 1)
glimpse(pepito_df)pepito_df |> count(weekday)Si glimpse() muestra hour <int> y date <date>, ¡perfecto! Los datos están listos.
Sobre el extra: los conteos por día suelen ser bastante parejos (un gato sale más o menos todos los días), pero pequeñas diferencias pueden aparecer. Lo interesante no es cuántas veces sale cada día, sino a qué hora y cuánto rato — eso lo exploraremos con gráficos en los próximos módulos.
La gramática de los gráficos: ggplot2 desde cero
El objetivo del módulo
Este es el módulo más importante de todo el curso. Aquí entenderás la idea central de ggplot2: la gramática de los gráficos. Una vez que la captes, no tendrás que memorizar gráficos: los vas a construir. Todos los demás módulos son variaciones de lo que aprendes aquí.
La idea: un gráfico se arma por capas
En ggplot2, un gráfico no se hace de una. Se construye sumando capas, como apilar transparencias una sobre otra. Cada capa aporta algo:
Las piezas de todo gráfico ggplot2
- Los datos (
data): la tabla de donde sale todo. Siempre un data frame. - El mapeo estético (
aes): qué columna va en el eje X, cuál en el eje Y, cuál define el color, etc. Es “conectar columnas con propiedades visuales”. - La geometría (
geom_*): la forma con la que se dibujan los datos. ¿Barras? ¿Puntos? ¿Líneas? Cada forma es ungeom_. - Las escalas (
scale_*): cómo se traducen los datos a lo visible (qué colores, qué marcas en los ejes…). Opcional. - Las etiquetas (
labs): título, nombres de ejes, leyenda. Opcional pero recomendado. - El tema (
theme_*): la apariencia general (fondo, tipografía, rejilla). Opcional.
Las tres primeras (datos, aes, geom) son obligatorias: sin ellas no hay gráfico. Las otras tres son para mejorar y pulir.
El esqueleto mínimo
Todo gráfico de ggplot2 sigue esta estructura. Apréndela de memoria, es el molde de todo:
ggplot(datos, aes(x = columna1, y = columna2)) + geom_algo()Léelo así: “Crea un gráfico con estos datos, conectando estas columnas a los ejes (aes), y dibújalos con esta forma (geom).”
Fíjate en el + al final de la primera línea: es el “pegamento” que suma capas. Cada nueva capa se agrega con un +.
El error clásico número uno: el `+` va al FINAL de la línea
El + que une capas debe ir al final de cada línea, nunca al principio de la siguiente. Esto está bien:
ggplot(pepito_df, aes(x = way)) + geom_bar()Esto está mal y R te dará un resultado a medias o un error:
ggplot(pepito_df, aes(x = way)) + geom_bar()¿Por qué? Porque al ver una línea que ya está “completa”, R cree que terminaste, y la línea siguiente que empieza con + la lee como algo aparte. Regla: el + siempre cierra la línea de arriba.
Tu primer gráfico, de verdad
Hagamos el gráfico más simple posible con los datos de Pépito: contar cuántas veces salió (out) y cuántas entró (in).
library(ggplot2)
ggplot(pepito_df, aes(x = way)) + geom_bar()¡Eso es todo! Obtienes dos barras: una para in y otra para out. Desarmemos qué pasó:
ggplot(pepito_df, ...)— empieza un gráfico usando la tablapepito_df.aes(x = way)— pon la columnawayen el eje X. No pusimosyporque…geom_bar()— …las barras cuentan solas cuántas filas hay de cada valor. El eje Y (la altura) lo calcula ggplot2 automáticamente.
¿Por qué `geom_bar()` no necesita `y`?
geom_bar() está pensado para contar. Tú le das una columna en X (las categorías) y él cuenta cuántas filas caen en cada una y usa eso como altura. Por eso no le diste un y: lo calculó él. (Más adelante, si ya tienes los totales calculados, usarás geom_col(), que sí espera un y.)
aes(): mapear vs. fijar un valor
Esta es la distinción que más confunde al empezar, y entenderla te ahorra horas. Hay dos formas de dar color (o cualquier propiedad visual) a un gráfico:
1. Mapear (color depende de una columna) → va dentro de aes():
ggplot(pepito_df, aes(x = way, fill = way)) + geom_bar()Aquí fill = way significa “pinta cada barra según su valor de way”. El color depende de los datos: cada categoría recibe un color distinto y aparece una leyenda.
2. Fijar (un color fijo para todo) → va fuera de aes(), dentro del geom_:
ggplot(pepito_df, aes(x = way)) + geom_bar(fill = "steelblue")Aquí todas las barras son azules. El color no depende de los datos, es una decisión estética constante, así que va fuera de aes().
El error de poner el color fijo dentro de `aes()`
Si escribes aes(fill = "steelblue") (con comillas, dentro de aes), no obtienes barras azules. ggplot2 interpreta "steelblue" como una “categoría” llamada literalmente steelblue, te pinta todo de un color raro y te agrega una leyenda absurda.
Regla simple:
- ¿El color depende de una columna? → dentro de
aes(), sin comillas:aes(fill = way). - ¿El color es fijo para todo? → fuera de
aes(), con comillas:geom_bar(fill = "steelblue").
Sumando más capas
Ahora veamos cómo crece un gráfico agregando capas con +. Partimos del básico y le ponemos títulos y un tema más limpio:
ggplot(pepito_df, aes(x = way, fill = way)) + geom_bar() + labs( title = "¿Pépito sale o entra más?", x = "Dirección", y = "Número de eventos", fill = "Sentido" ) + theme_minimal()Lee las capas de arriba a abajo:
ggplot(...)+aes(...)— los datos y el mapeo.geom_bar()— las barras.labs(...)— los textos:title(título),xey(nombres de ejes),fill(título de la leyenda).theme_minimal()— un tema con fondo blanco y rejilla suave, mucho más limpio que el gris por defecto.
Cada + agregó una transparencia encima. Así se construye cualquier gráfico de ggplot2, por complejo que sea: empiezas simple y vas sumando.
Construye de a poco
No intentes escribir el gráfico final de una sola vez. Empieza con ggplot(...) + geom_...(), mira el resultado, y agrega una capa a la vez. Si algo se ve raro, sabes que fue la última capa que pusiste. Esta es la forma más rápida de trabajar y de aprender.
El patrón mental
Cuando quieras hacer cualquier gráfico, hazte estas tres preguntas en orden:
- ¿Qué datos? → eso va en
ggplot(datos, ...). - ¿Qué columna en cada parte visual? (X, Y, color…) → eso va en
aes(...). - ¿Con qué forma los dibujo? → eso es el
geom_.
Responder esas tres preguntas es hacer el gráfico. Todo lo demás (escalas, etiquetas, temas) es pulido.
Ejercicio
Ejercicio 5 — Domina el esqueleto
- Haz el gráfico de barras más simple de
way(sologgplot+geom_bar). - Ahora píntalo: agrega
fill = waydentro deaes(). Observa la leyenda que aparece. - Cambia de idea: quita el
filldeaes()y pongeom_bar(fill = "darkorange"). ¿Qué diferencia ves? - Agrega
labs()con un título y nombres de ejes, ytheme_minimal(). - A propósito, escribe
aes(fill = "darkorange")(con comillas, dentro de aes) y observa el resultado raro. Así reconocerás ese error cuando te pase de verdad.
Ver solución
library(ggplot2)
# 1. El más simpleggplot(pepito_df, aes(x = way)) + geom_bar()
# 2. Color que depende de los datos (dentro de aes)ggplot(pepito_df, aes(x = way, fill = way)) + geom_bar()
# 3. Color fijo (fuera de aes)ggplot(pepito_df, aes(x = way)) + geom_bar(fill = "darkorange")
# 4. Con etiquetas y temaggplot(pepito_df, aes(x = way, fill = way)) + geom_bar() + labs(title = "¿Pépito sale o entra más?", x = "Dirección", y = "Número de eventos", fill = "Sentido") + theme_minimal()
# 5. El error a propósito: barras de un color raro + leyenda absurdaggplot(pepito_df, aes(x = way)) + geom_bar(aes(fill = "darkorange"))Lo que deberías notar:
- En el (2), cada barra tiene su color y aparece una leyenda (porque el color mapea una columna).
- En el (3), todas las barras son naranjas y no hay leyenda (el color es fijo).
- En el (5), las barras salen de un color cualquiera y aparece una leyenda que dice “darkorange” como si fuera una categoría. Ese es exactamente el síntoma de haber metido un color fijo dentro de
aes().
Si entendiste la diferencia entre mapear (dentro de aes) y fijar (fuera de aes), ya tienes la mitad de ggplot2 dominada.
Tu primer gráfico: barras con geom_bar
El objetivo del módulo
Ya conoces la gramática. Ahora vamos a usarla para responder una pregunta real: ¿a qué hora del día está más activo Pépito? En el camino aprenderás a controlar las barras: agrupar por color, separarlas o apilarlas, y ajustar el eje X.
De categorías a números: la hora del día
En el módulo 5 graficamos way, que tiene solo dos valores (in/out). Ahora usaremos la columna hour, que va de 0 a 23. Empecemos simple, contando los eventos por hora:
library(ggplot2)
ggplot(pepito_df, aes(x = hour)) + geom_bar()Obtienes 24 barras, una por hora. Ya se nota la rutina del gato: pocas barras de madrugada (Pépito duerme) y barras altas durante el día.
Separar por sentido con fill
La pregunta se pone más interesante si distinguimos entradas de salidas. Para eso mapeamos way al color de relleno (fill), como aprendimos:
ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar()Esto apila las barras: dentro de cada hora, una parte es in y otra out, una encima de la otra. Está bien, pero comparar alturas apiladas es difícil. Mejor pongámoslas lado a lado.
position: apilar, separar o normalizar
El argumento position de geom_bar() controla qué pasa cuando hay varios grupos por barra. Hay tres opciones que conviene conocer:
# Lado a lado (lo que queremos): dos barritas por horaggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "dodge")position = "stack"(el de por defecto) — apila los grupos uno sobre otro. Bueno para ver el total.position = "dodge"— los pone lado a lado. Bueno para comparar grupos entre sí. Es el que usaremos.position = "fill"— apila pero estira todas las barras a la misma altura (100%), para ver proporciones. Bueno para “¿qué porcentaje de eventos a esta hora son salidas?”.
Detente y piensa
Elegir position no es decoración: cambia la pregunta que responde el gráfico. stack responde “¿cuántos eventos hubo?”; dodge responde “¿hubo más entradas o salidas a esta hora?”; fill responde “¿qué proporción?”. Elige según lo que quieras mostrar.
El gráfico completo
Juntemos todo y pulámoslo. Este es el gráfico de actividad por hora, completo:
ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "dodge") + scale_x_continuous(breaks = 0:23) + labs( title = "Actividad de Pépito por hora del día", x = "Hora del día", y = "Número de eventos", fill = "Sentido" ) + theme_minimal()La capa nueva es scale_x_continuous(breaks = 0:23). Sirve para controlar las marcas del eje X:
scale_x_continuousse usa cuando el eje X es numérico (comohour).breaks = 0:23le dice “pon una marca en cada número del 0 al 23”. (0:23es la forma corta de R para “todos los enteros de 0 a 23”.)
Sin esta línea, ggplot2 pone marcas cada 5 horas (0, 5, 10…) y cuesta leer la hora exacta. Con ella, ves las 24 horas etiquetadas.
Lo que revela el gráfico
Vas a ver dos cosas: que la actividad se concentra en las horas de luz y cae de madrugada, y que las salidas (out) tienden a dominar más temprano y las entradas (in) un poco más tarde — lo lógico en un gato que sale a rondar y vuelve después. Acabas de leer la rutina de un gato en un gráfico. 🐱
Errores comunes con barras
Error: `geom_bar()` me pide un `y` o me da error con `stat`
Si ya tienes los totales calculados (por ejemplo, una tabla con una columna n) y se los pasas a geom_bar(), choca, porque geom_bar() quiere contar él mismo. En ese caso usa geom_col(), que dibuja la altura que tú le das:
conteo <- pepito_df |> count(hour)ggplot(conteo, aes(x = hour, y = n)) + geom_col()Regla: geom_bar() cuenta filas por ti; geom_col() dibuja totales que ya calculaste.
Error: las barras de `dodge` quedan de distinto ancho
Si algún grupo no tiene datos en cierta categoría, con position = "dodge" las barras pueden verse desalineadas. Para barras de ancho consistente, usa position = position_dodge(preserve = "single"). No te preocupes por esto ahora; tenlo guardado por si te pasa.
`scale_x_continuous` vs `scale_x_discrete`
- Si tu eje X es un número (
hour), usascale_x_continuous(). - Si tu eje X es una categoría/texto (
way,weekday), usascale_x_discrete().
Poner el scale_ equivocado es una causa silenciosa de “no me hace caso el breaks”. Mira el tipo de tu columna con glimpse() si tienes dudas.
Ejercicio
Ejercicio 6 — Explora las barras
- Haz el gráfico de actividad por hora con
position = "dodge", las marcas del 0 al 23, títulos ytheme_minimal(). - Cambia
positiona"fill". ¿Qué pregunta responde ahora el gráfico? - Cambia
positiona"stack". ¿Cuándo te serviría más esta versión? - Extra: haz un gráfico de barras de
weekday(día de la semana). ¿Necesitasscale_x_continuousoscale_x_discretepara este? ¿Por qué?
Ver solución
library(ggplot2)
# 1. Dodge (lado a lado)ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "dodge") + scale_x_continuous(breaks = 0:23) + labs(title = "Actividad de Pépito por hora del día", x = "Hora del día", y = "Número de eventos", fill = "Sentido") + theme_minimal()
# 2. Fill (proporción 0–100%)ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "fill") + scale_x_continuous(breaks = 0:23) + labs(title = "Proporción de entradas y salidas por hora", x = "Hora del día", y = "Proporción", fill = "Sentido") + theme_minimal()
# 4. Día de la semana (eje categórico)ggplot(pepito_df, aes(x = weekday)) + geom_bar(fill = "steelblue") + labs(title = "Actividad por día de la semana", x = "Día", y = "Número de eventos") + theme_minimal()Respuestas:
- (2)
fillresponde “de los eventos de esta hora, ¿qué porcentaje son entradas y qué porcentaje salidas?”. Útil para ver el balance, no la cantidad. - (3)
stacksirve cuando te importa el total por hora y, de paso, su composición. - (4)
weekdayes una categoría (texto ordenado), así que el eje es discreto: usaríasscale_x_discrete()si quisieras tocarlo.scale_x_continuousno funcionaría aquí porque el eje no es numérico.
Series de tiempo: la actividad a lo largo de los años
El objetivo del módulo
La gracia del dataset de Pépito es que abarca casi 14 años. En este módulo vamos a mirar cómo cambia su actividad a lo largo del tiempo: gráficos donde el eje X es una fecha. Aprenderás dos formas de hacerlo: geom_freqpoly() (que cuenta solo) y geom_line() (que dibuja totales que tú calculas).
Contar eventos por día con geom_freqpoly()
Queremos ver cuántos eventos hubo cada día, a lo largo de todos los años. geom_freqpoly() es perfecto: toma una columna continua (como la fecha), la divide en tramos y dibuja una línea con la cantidad de eventos en cada tramo.
library(ggplot2)
ggplot(pepito_df, aes(x = date)) + geom_freqpoly(binwidth = 1) + labs( title = "Actividad de Pépito a lo largo del tiempo", x = "Fecha", y = "Eventos por día" ) + theme_minimal()Línea por línea:
aes(x = date)— el eje X es la fecha. No damosy: igual quegeom_bar(), este geom cuenta solo.geom_freqpoly(binwidth = 1)— agrupa los datos en tramos de 1 día (binwidth = 1, porque la fecha se mide en días) y une los conteos con una línea. Resultado: un punto por día, conectado.- El resto son etiquetas y tema, ya conocidos.
`geom_freqpoly` es como un `geom_histogram`, pero con línea
Los dos hacen lo mismo —cuentan cuántos datos caen en cada tramo— pero uno lo dibuja con barras (geom_histogram) y el otro con una línea (geom_freqpoly). Para series largas de tiempo, la línea suele verse más limpia. Veremos histogramas en detalle en el módulo 8.
Detente y piensa
Mira el gráfico: ¿hay tramos donde la línea cae a cero? Esos podrían ser periodos en que el sensor estuvo apagado, o el gato/dueño de vacaciones. Los huecos en los datos también cuentan una historia — y es importante notarlos antes de sacar conclusiones.
La otra forma: calcular y dibujar con geom_line()
geom_freqpoly() cuenta por ti, pero muchas veces querrás calcular tú un total (por día, por mes, por año) y luego dibujarlo. Ese patrón —resumir primero, graficar después— es uno de los más usados en análisis de datos. Vamos a verlo contando eventos por día con dplyr y dibujándolos con geom_line():
library(dplyr)library(ggplot2)
# Paso 1: resumir — contar eventos por díapor_dia <- pepito_df |> count(date)
# Paso 2: graficar — date en X, n (el conteo) en Yggplot(por_dia, aes(x = date, y = n)) + geom_line() + labs(title = "Eventos por día", x = "Fecha", y = "Número de eventos") + theme_minimal()La diferencia clave con geom_freqpoly: aquí sí damos y = n, porque ya calculamos el conteo con count(date). geom_line() no cuenta nada; solo une con una línea los puntos (x, y) que le pasas.
`count()` crea una columna llamada `n`
Cuando haces pepito_df |> count(date), dplyr te devuelve una tabla con dos columnas: date (cada fecha distinta) y n (cuántas filas había de esa fecha). Por eso en el aes() usamos y = n. Si quieres otro nombre, puedes usar count(date, name = "eventos").
Suavizar la mirada: agrupar por mes
Un punto por día durante 14 años es mucho ruido. A veces conviene mirar más “de lejos”, por ejemplo por mes. Aquí combinamos lubridate y dplyr:
library(lubridate)
por_mes <- pepito_df |> mutate(mes = floor_date(date, "month")) |> # redondea cada fecha al 1º de su mes count(mes)
ggplot(por_mes, aes(x = mes, y = n)) + geom_line() + geom_point() + labs(title = "Eventos por mes", x = "Mes", y = "Número de eventos") + theme_minimal()Dos cosas nuevas:
floor_date(date, "month")— “redondea hacia abajo” cada fecha al primer día de su mes. Así todas las fechas de noviembre de 2011 se vuelven2011-11-01y se pueden contar juntas.geom_point()sumado ageom_line()— ¡otra vez las capas! Dibujamos la línea y encima los puntos. Esto muestra muy bien cómo se apilan capas: ambos geoms comparten el mismoaes()delggplot().
Cambia el nivel de detalle con `floor_date`
floor_date(date, "week"), "month", "quarter", "year"… Cambiando una sola palabra controlas cuán “de cerca” o “de lejos” miras la serie. Es una de las herramientas más útiles para series de tiempo: empieza de lejos (por año o mes) para ver la tendencia general, y acércate (por día) solo donde necesites el detalle.
Errores comunes en series de tiempo
Error: la línea hace zigzags imposibles
geom_line() une los puntos en el orden en que vienen en la tabla. Si tus datos no están ordenados por fecha, la línea salta para adelante y atrás formando una maraña. Solución: ordena por la columna del eje X antes de graficar:
por_dia <- pepito_df |> count(date) |> arrange(date)Con count() los datos suelen venir ya ordenados, pero si calculas el resumen de otra forma, ordena siempre antes de geom_line().
Error: el eje X muestra números raros en vez de fechas
Si el eje X aparece como números grandes (tipo 15000) en lugar de fechas, es que la columna no es de tipo fecha, sino número o texto. Vuelve al módulo 4 y asegúrate de haber creado date con as_date(). Compruébalo con glimpse(): debe decir <date>.
Controlar el formato del eje de fechas
Para elegir cómo se ven las fechas en el eje (por ejemplo, mostrar solo el año), existe scale_x_date():
scale_x_date(date_breaks = "2 years", date_labels = "%Y")date_breaks = "2 years" pone una marca cada dos años; date_labels = "%Y" muestra solo el año. Guárdalo para cuando un eje de fechas se vea apretado.
Ejercicio
Ejercicio 7 — Mira el tiempo a distintas escalas
- Haz el gráfico de eventos por día con
geom_freqpoly(binwidth = 1). - Calcula los eventos por mes con
floor_date+count, y grafícalos congeom_line() + geom_point(). - Calcula los eventos por año (cambia
"month"por"year"enfloor_date) y grafícalos. ¿Se nota algún año con menos datos? - Extra: al gráfico anual, agrégale
scale_x_date(date_breaks = "1 year", date_labels = "%Y")para etiquetar cada año.
Ver solución
library(dplyr)library(lubridate)library(ggplot2)
# 1. Por día con freqpolyggplot(pepito_df, aes(x = date)) + geom_freqpoly(binwidth = 1) + labs(title = "Eventos por día", x = "Fecha", y = "Eventos") + theme_minimal()
# 2. Por mespor_mes <- pepito_df |> mutate(mes = floor_date(date, "month")) |> count(mes)
ggplot(por_mes, aes(x = mes, y = n)) + geom_line() + geom_point() + labs(title = "Eventos por mes", x = "Mes", y = "Eventos") + theme_minimal()
# 3 y 4. Por año, con eje etiquetadopor_anio <- pepito_df |> mutate(anio = floor_date(date, "year")) |> count(anio)
ggplot(por_anio, aes(x = anio, y = n)) + geom_line() + geom_point() + scale_x_date(date_breaks = "1 year", date_labels = "%Y") + labs(title = "Eventos por año", x = "Año", y = "Eventos") + theme_minimal()Sobre el (3): es normal que el primer y el último año tengan menos eventos, porque los datos empiezan en noviembre de 2011 (año incompleto) y terminan cuando se descargó el archivo (último año también incompleto). Detectar años incompletos es importante: si no, podrías concluir erróneamente que “Pépito salió mucho menos en 2011” cuando en realidad solo hay dos meses de datos ese año.
Distribuciones: histogramas y densidad
El objetivo del módulo
Hasta ahora contamos eventos. Pero la pregunta más rica del dataset es otra: ¿cuánto tiempo pasa Pépito fuera de casa? Para responderla necesitamos algo nuevo: emparejar cada salida con la entrada siguiente y medir la diferencia. Eso nos dará las “sesiones de paseo”, y con ellas haremos histogramas y densidades: los gráficos para ver cómo se distribuye una variable numérica.
Este módulo mezcla un poco más de preparación de datos con gráficos nuevos. Vale la pena: el resultado es la base del gran final.
El problema: cada fila es un evento, no una salida completa
Nuestros datos tienen una fila por evento (salió, o entró). Pero una “salida al mundo” son en realidad dos eventos: un out y, más tarde, el in que lo cierra. Para medir la duración necesitamos mirar cada fila y la siguiente.
La función que mira “la fila siguiente” es lead() de dplyr. Veámosla primero con las transiciones:
library(dplyr)
pepito_df |> mutate(next_way = lead(way)) |> count(way, next_way)lead(way) crea una columna con el valor de way de la fila siguiente. Así podemos contar qué pasa después de cada evento: ¿una salida suele ir seguida de una entrada? El conteo te muestra las combinaciones (out → in, out → out, etc.).
Antes de usar `lead()`: ¡ORDENA los datos!
lead() toma “la fila de abajo” tal como está en la tabla. Si las filas no están en orden cronológico, “la siguiente” no será el evento que ocurrió después en el tiempo, y todo el cálculo quedará mal.
Por eso, lo primero es ordenar por fecha-hora:
pepito_df <- pepito_df |> arrange(datetime_fr)arrange() ordena las filas; aquí, de la más antigua a la más reciente. Nunca uses lead() o lag() sin ordenar primero. Es el error silencioso más peligroso de este módulo.
Construir las sesiones de paseo
Ahora sí, armamos la tabla de sesiones. La lógica es: para cada fila que sea una salida (out) cuyo evento siguiente sea una entrada (in), calculamos cuánto tiempo pasó entre ambos.
library(dplyr)
# 1. Ordenar cronológicamente (imprescindible)pepito_df <- pepito_df |> arrange(datetime_fr)
# 2. Construir las sesionessessions <- pepito_df |> mutate( next_way = lead(way), # el sentido del evento siguiente next_time = lead(datetime_fr) # la hora del evento siguiente ) |> filter(way == "out", next_way == "in") |> # solo: salió y luego entró mutate( duracion_min = as.numeric(difftime(next_time, datetime_fr, units = "mins")), duracion_horas = duracion_min / 60, salida = datetime_fr, entrada = next_time ) |> select(salida, entrada, duracion_min, duracion_horas)Léelo como una receta encadenada con |>:
mutate(next_way = ..., next_time = ...)— agregamos dos columnas: el sentido y la hora del evento siguiente.filter(way == "out", next_way == "in")— nos quedamos solo con las filas que son una salida seguida de una entrada. Una coma dentro defilter()significa “y” (ambas condiciones). Fíjate en el==doble: en R,==compara, mientras que=asigna. Para filtrar siempre va==.mutate(duracion_min = ...)— calculamos la duración.difftime(fin, inicio, units = "mins")da la diferencia de tiempo en minutos;as.numeric()la convierte en un número normal con el que podemos graficar. También guardamossalidayentradacon nombres claros.select(...)— nos quedamos solo con las cuatro columnas que nos interesan.
El resultado, sessions, es una tabla donde cada fila es una salida completa de Pépito, con cuánto duró.
`difftime()`, tu calculadora de tiempo
Restar dos fechas-hora a mano es complicado (¿horas?, ¿días?, ¿segundos?). difftime(a, b, units = "mins") lo resuelve: te da la diferencia exacta en la unidad que pidas ("mins", "hours", "days"…). Envolverla en as.numeric() es importante para que ggplot2 la trate como un número y no como un objeto de tiempo raro.
Limpiar sesiones imposibles
Los datos reales tienen ruido. A veces un evento se pierde (el sensor falla) y queda una “sesión” de duración negativa o de varios días. Las descartamos:
sessions <- sessions |> filter( duracion_min > 0, # nada de duraciones negativas o cero duracion_horas < 24 # descarta paseos "de más de un día" (evento perdido) )¿Y si sale de noche y vuelve de madrugada (otro día)?
Buena duda: ¿una salida que cruza la medianoche se calcula bien? Sí. La duración se obtiene con difftime(next_time, datetime_fr), y esas dos columnas son fechas-hora completas (día + hora), no solo la hora. difftime resta los dos instantes reales, así que cruzar la medianoche no importa: si sale el 31-dic a las 23:30 y vuelve el 1-ene a las 02:00, la duración es 150 min (2,5 horas), exacta.
Lo único a tener presente es a qué momento se le atribuye la salida: usamos salida (el evento out), por lo que esa sesión cuenta en el día, mes, año y hora en que salió (31-dic, hora 23), no en que volvió. Es la convención natural, y por eso más adelante agrupamos por la fecha de salida. El filtro duracion_horas < 24 no afecta estas salidas nocturnas normales (duran pocas horas); solo descarta los casos en que se perdió el evento de entrada y la “sesión” abarcaría días enteros.
Detente y piensa
Decidir qué datos descartar es una decisión de análisis, no solo técnica. Aquí asumimos que un gato no pasa más de 24 horas seguidas fuera; si pasa, probablemente se perdió el evento de entrada. Es una suposición razonable, pero siempre conviene anotar qué filtraste y por qué, para que tu análisis sea transparente.
Por fin: el histograma “¿cuánto tiempo pasa fuera?”
Un histograma divide una variable numérica en tramos y cuenta cuántos datos caen en cada uno. Es el gráfico para ver la forma de una distribución: ¿la mayoría de los paseos son cortos? ¿hay algunos larguísimos?
library(ggplot2)
ggplot(sessions, aes(x = duracion_min)) + geom_histogram(binwidth = 15) + # tramos de 15 minutos labs( title = "¿Cuánto tiempo pasa Pépito fuera?", x = "Minutos fuera", y = "Número de salidas" ) + theme_minimal()aes(x = duracion_min)— la variable que queremos distribuir. Solo X; el Y (el conteo) lo calcula el histograma.geom_histogram(binwidth = 15)— agrupa en tramos de 15 minutos. El ancho del tramo lo eliges tú conbinwidth.
Vas a ver una distribución con una cola larga: muchísimos paseos cortos y unos pocos muy largos. Eso aplasta el gráfico hacia la izquierda.
`binwidth` vs `bins`: elige UNO
geom_histogram() tiene dos formas de definir los tramos:
binwidth = 15— fija el ancho de cada tramo (15 minutos). Más interpretable.bins = 40— fija el número de tramos (40 en total).
Usa uno o el otro, no ambos. Y siempre define alguno: si no, ggplot2 usa 30 tramos por defecto y te avisa con un mensaje (stat_bin() using bins = 30. Pick better value...). Ese mensaje no es un error, es un recordatorio de que elijas tú el tramo.
Domando la cola larga con escala logarítmica
Cuando hay pocos valores enormes y muchos pequeños, una escala logarítmica en el eje X reparte mejor el espacio y deja ver la forma real:
ggplot(sessions, aes(x = duracion_min)) + geom_histogram(bins = 40) + scale_x_log10() + labs( title = "Duración de las salidas (escala log)", x = "Minutos fuera (log)", y = "Salidas" ) + theme_minimal()La capa nueva es scale_x_log10(): comprime el eje X de forma logarítmica. Ahora la distribución se ve como una “campana” mucho más legible, y se nota la duración típica de un paseo.
`scale_x_log10()` y los ceros
El logaritmo de 0 (o de un número negativo) no existe. Si tu variable tiene ceros o negativos, scale_x_log10() los descarta y te avisa con un warning. Por eso fue importante el filtro duracion_min > 0 de antes: deja la variable lista para la escala log.
Densidad: entradas vs. salidas a lo largo del día
Una curva de densidad es como un histograma “suavizado”: en vez de barras, una curva continua que muestra dónde se concentran los datos. Sirve muy bien para comparar dos grupos. Volvamos a pepito_df para comparar a qué horas sale y a qué horas entra:
ggplot(pepito_df, aes(x = hour, fill = way)) + geom_density(alpha = 0.5) + scale_x_continuous(breaks = seq(0, 24, 2)) + labs( title = "Distribución horaria: entradas vs salidas", x = "Hora del día", y = "Densidad", fill = "Sentido" ) + theme_minimal()Dos detalles nuevos:
geom_density(alpha = 0.5)— dibuja una curva por cada valor deway.alpha = 0.5la hace medio transparente, para que se vean las dos curvas aunque se solapen.alphava de 0 (invisible) a 1 (opaco) y va fuera deaes(), porque es un valor fijo.scale_x_continuous(breaks = seq(0, 24, 2))— marcas cada 2 horas.seq(0, 24, 2)genera la secuencia 0, 2, 4, …, 24.
Las dos curvas revelan el patrón: las salidas se concentran en ciertas horas y las entradas en otras, un poco más tarde.
`alpha` lo arregla casi todo cuando las cosas se tapan
Cada vez que dos formas se solapen y no se distingan (curvas de densidad, nubes de puntos densas, barras encimadas), prueba bajar el alpha. Es el truco más rápido para que un gráfico saturado vuelva a ser legible.
Un vistazo numérico con summary()
Antes de seguir, conviene mirar los números crudos. summary() da un resumen rápido de cada columna:
summary(sessions)Te muestra el mínimo, máximo, promedio y la mediana de duracion_min. Mira la mediana: suele ser mucho menor que el promedio, justo porque la cola larga estira el promedio hacia arriba. Por eso, para “el tiempo típico fuera”, la mediana es más honesta que el promedio — algo que usaremos en el mapa de calor final.
Ejercicio
Ejercicio 8 — Construye las sesiones y su distribución
- Ordena
pepito_dfpordatetime_fr. - Construye la tabla
sessions(salida → entrada, conduracion_min). - Filtra las sesiones imposibles (
duracion_min > 0,duracion_horas < 24). - Haz el histograma de
duracion_minconbinwidth = 15. - Haz la versión con
scale_x_log10(). - Ejecuta
summary(sessions)y compara la mediana con el promedio deduracion_min. ¿Cuál es mayor? ¿Por qué crees que pasa?
Ver solución
library(dplyr)library(ggplot2)
# 1–3. Construir y limpiar sesionespepito_df <- pepito_df |> arrange(datetime_fr)
sessions <- pepito_df |> mutate(next_way = lead(way), next_time = lead(datetime_fr)) |> filter(way == "out", next_way == "in") |> mutate( duracion_min = as.numeric(difftime(next_time, datetime_fr, units = "mins")), duracion_horas = duracion_min / 60, salida = datetime_fr, entrada = next_time ) |> select(salida, entrada, duracion_min, duracion_horas) |> filter(duracion_min > 0, duracion_horas < 24)
# 4. Histograma normalggplot(sessions, aes(x = duracion_min)) + geom_histogram(binwidth = 15) + labs(title = "¿Cuánto tiempo pasa Pépito fuera?", x = "Minutos fuera", y = "Número de salidas") + theme_minimal()
# 5. Con escala logarítmicaggplot(sessions, aes(x = duracion_min)) + geom_histogram(bins = 40) + scale_x_log10() + labs(title = "Duración de las salidas (escala log)", x = "Minutos fuera (log)", y = "Salidas") + theme_minimal()
# 6. Resumensummary(sessions)Sobre la mediana vs. el promedio: el promedio es mayor que la mediana. Esto pasa porque unas pocas salidas larguísimas (la cola) tiran del promedio hacia arriba, mientras que la mediana —el valor del medio— no se deja arrastrar por esos extremos. Por eso, para describir “lo normal” en datos con cola larga, la mediana es la mejor amiga. Lo recordaremos en el módulo 11.
Comparar grupos: boxplots
El objetivo del módulo
Un histograma muestra la distribución de una variable. Pero a menudo queremos comparar una distribución entre varios grupos: ¿pasa Pépito más rato fuera unos años que otros? ¿depende de la hora a la que sale? El gráfico perfecto para eso es el boxplot (diagrama de caja).
¿Qué muestra un boxplot?
Antes de dibujarlo, entiende qué cuenta. Una “caja” resume una distribución en cinco números:
Anatomía de una caja
- La línea del medio de la caja es la mediana (el valor central: la mitad de los datos está por debajo).
- Los bordes de la caja son el primer y tercer cuartil: dentro de la caja está el 50% central de los datos.
- Las líneas que salen de la caja (los “bigotes”) llegan hasta los valores más extremos razonables.
- Los puntos sueltos más allá de los bigotes son posibles valores atípicos (outliers).
En una sola figura compacta puedes comparar muchos grupos de un vistazo: dónde está su centro y cuán dispersos son.
Boxplot por año
Queremos una caja por cada año. Primero necesitamos una columna de año en sessions:
library(lubridate)
sessions$year <- year(sessions$salida)year() extrae el año de la fecha de salida. Ahora el gráfico:
library(ggplot2)
ggplot(sessions, aes(x = factor(year), y = duracion_min)) + geom_boxplot() + scale_y_log10() + labs( title = "¿Pépito sale más o menos con los años?", x = "Año", y = "Minutos fuera (escala log)" ) + theme_minimal()Hay dos detalles que debes entender bien:
x = factor(year)— envolvemosyearenfactor(). ¿Por qué? Porque el año es un número (2011, 2012…), y si lo dejamos como número, ggplot2 cree que el eje X es continuo y dibuja una sola caja gigante.factor()convierte el año en categorías separadas, una por año, y así obtenemos una caja por año. Este es el truco clave de los boxplots.scale_y_log10()— como las duraciones tienen cola larga (módulo 8), la escala logarítmica en el eje Y hace que las cajas se vean proporcionadas en vez de aplastadas abajo.
El error más típico del boxplot: una sola caja gigante
Si tu boxplot muestra una única caja cuando esperabas varias, casi siempre es porque el eje X es numérico continuo. La solución es convertirlo en categoría con factor():
aes(x = factor(year), y = duracion_min) # ✅ una caja por añoaes(x = year, y = duracion_min) # ❌ una sola caja para todoEsto aplica siempre que tu “grupo” sea un número (años, meses, horas): envuélvelo en factor().
Comprobaciones rápidas antes de concluir
Antes de leer un gráfico, conviene revisar los datos crudos para no sacar conclusiones falsas. Tres comprobaciones útiles:
sessions$year <- factor(year(sessions$salida))
table(sessions$year) # cuántas salidas hay por añorange(sessions$salida) # primera y última fechasummary(sessions$duracion_min) # min, mediana, promedio, maxtable()cuenta cuántas filas hay de cada año. Sirve para detectar años con pocos datos (el primero y el último suelen estar incompletos).range()te da el primer y último instante registrado.summary()resume la variable numérica.
Detente y piensa
Si un año tiene muy pocas salidas (porque está incompleto o el sensor estuvo apagado), su caja se construye con poca información y puede engañar. Por eso miramos table() antes: una caja “rara” en un año con 12 salidas no significa lo mismo que una en un año con 3.000. Mirar el conteo te protege de conclusiones apresuradas.
Boxplot por hora de salida
Cambiemos la pregunta: ¿los paseos que empiezan a cierta hora duran más? Quizás los de la noche son más largos que los del mediodía. Aquí encadenamos el cálculo y el gráfico con |>:
library(dplyr)library(ggplot2)
sessions |> mutate(hora_salida = hour(salida)) |> ggplot(aes(x = factor(hora_salida), y = duracion_min)) + geom_boxplot() + scale_y_log10() + labs( title = "Duración de la salida según la hora en que sale", x = "Hora de salida (0–23)", y = "Minutos fuera (log)" ) + theme_minimal()Fíjate en algo importante de sintaxis: mientras preparamos datos usamos el pipe |>, pero al construir el gráfico cambiamos a +. Son dos “pegamentos” distintos: |> encadena pasos de datos, + encadena capas de ggplot2. Aquí conviven: mutate(...) |> ggplot(...) + geom_boxplot() + ....
No mezcles `|>` y `+`
Un error clásico al encadenar todo junto es poner |> donde va + (o al revés). Recuerda:
- Entre pasos de dplyr (
filter,mutate,count,arrange…): usa|>. - Entre capas de ggplot2 (
geom_,labs,scale_,theme_…): usa+.
El momento exacto del cambio es justo en ggplot(...): lo que viene antes se conecta con |>; lo que viene después (las capas) con +.
Errores comunes con boxplots
Error: `Discrete value supplied to continuous scale` (o al revés)
Si pones scale_y_log10() pero el eje Y no es numérico, o scale_x_continuous() sobre un eje que hiciste factor(), ggplot2 reclama. Solución: la escala debe coincidir con el tipo del eje. El eje X de un boxplot suele ser discreto (factor), y el Y continuo (la variable numérica).
Añade los puntos por encima de la caja
A veces quieres ver los datos reales además del resumen. Súmale una capa de puntos con dispersión horizontal:
geom_boxplot(outlier.shape = NA) +geom_jitter(width = 0.2, alpha = 0.2)outlier.shape = NA oculta los outliers de la caja (para no dibujarlos dos veces) y geom_jitter() esparce los puntos para que no se encimen. Otra vez: capas sobre capas.
Ejercicio
Ejercicio 9 — Compara distribuciones
- Crea la columna
yearensessionsy haz el boxplot por año confactor(year)yscale_y_log10(). - A propósito, haz el mismo gráfico sin
factor()(usandox = year). Observa la caja única y entiende el error. - Revisa
table(factor(year(sessions$salida))). ¿Qué años tienen menos salidas? - Haz el boxplot de
duracion_minpor hora de salida (factor(hour(salida))). - Extra: ¿a qué hora del día parecen empezar los paseos más largos? Mira las medianas de las cajas.
Ver solución
library(dplyr)library(lubridate)library(ggplot2)
# 1. Boxplot por año (correcto)sessions$year <- year(sessions$salida)
ggplot(sessions, aes(x = factor(year), y = duracion_min)) + geom_boxplot() + scale_y_log10() + labs(title = "¿Pépito sale más o menos con los años?", x = "Año", y = "Minutos fuera (escala log)") + theme_minimal()
# 2. El error a propósito: una sola cajaggplot(sessions, aes(x = year, y = duracion_min)) + geom_boxplot() + scale_y_log10()
# 3. Conteo por añotable(factor(year(sessions$salida)))
# 4. Por hora de salidasessions |> mutate(hora_salida = hour(salida)) |> ggplot(aes(x = factor(hora_salida), y = duracion_min)) + geom_boxplot() + scale_y_log10() + labs(title = "Duración según la hora de salida", x = "Hora de salida (0–23)", y = "Minutos fuera (log)") + theme_minimal()Lo que deberías observar:
- En el (2), una sola caja confirma por qué
factor()es indispensable. - En el (3), el primer y último año suelen tener menos salidas (datos incompletos), así que sus cajas hay que leerlas con cuidado.
- En el (5), suele notarse que los paseos que empiezan de noche o madrugada tienden a durar más (mediana más alta) que los del mediodía. ¡Pépito es más aventurero de noche!
Tendencias y estaciones: geom_smooth y líneas
El objetivo del módulo
Con casi 14 años de datos, la pregunta natural es: ¿cambió la rutina de Pépito con el tiempo? ¿Sale más o menos que antes? ¿Influye la estación del año? En este módulo aprendemos dos herramientas: geom_smooth() (líneas de tendencia) y cómo dibujar varias líneas a la vez usando el color para distinguir grupos.
La tendencia general con geom_smooth()
Si dibujáramos un punto por cada salida (miles de puntos), veríamos una nube imposible de leer. geom_smooth() resuelve esto: traza una curva suave que sigue la tendencia general entre tanto punto.
library(ggplot2)
ggplot(sessions, aes(x = salida, y = duracion_min)) + geom_smooth() + scale_y_log10() + labs( title = "Tendencia del tiempo fuera (2011–2024)", x = "Fecha", y = "Minutos fuera (log)" ) + theme_minimal()aes(x = salida, y = duracion_min)— la fecha de salida contra la duración.geom_smooth()— calcula y dibuja la curva de tendencia, con una banda gris alrededor que indica la incertidumbre (cuán segura es la curva en cada punto).scale_y_log10()— otra vez, por la cola larga de las duraciones.
La curva te dice, de un vistazo, si la duración típica de los paseos sube, baja o se mantiene a lo largo de los años.
¿Qué curva dibuja `geom_smooth()`?
Por defecto, con muchos datos usa un método llamado LOESS, que es una curva flexible que se adapta a la forma de los datos sin que tú le impongas una fórmula. Si quisieras una línea recta (tendencia lineal), se la pides con geom_smooth(method = "lm") (lm = modelo lineal). La curva flexible es buena para explorar; la recta, para resumir una tendencia simple.
Mensaje (no error) de `geom_smooth()`
Al usar geom_smooth() verás en la consola algo como geom_smooth() using method = 'loess' and formula 'y ~ x'. No es un error, solo te informa qué método eligió. Puedes ignorarlo o, si quieres silenciarlo, especificar tú el método.
Preparar las estaciones del año
Ahora la pregunta estacional. Necesitamos clasificar cada salida según la estación. Como Pépito vive en el hemisferio norte, los meses de verano son junio–agosto. Para crear la columna usamos case_when(), que es como un “si esto, entonces aquello”:
library(dplyr)library(lubridate)
sessions <- sessions |> mutate( anio = year(salida), estacion = case_when( month(salida) %in% c(12, 1, 2) ~ "Invierno", month(salida) %in% 3:5 ~ "Primavera", month(salida) %in% 6:8 ~ "Verano", TRUE ~ "Otoño" ) )Entendamos case_when():
- Cada línea tiene la forma
condición ~ valor. Se lee: “si se cumple la condición, asigna este valor”. month(salida) %in% c(12, 1, 2)— “si el mes es diciembre, enero o febrero”. El operador%in%pregunta “¿está dentro de esta lista?”.3:5es la forma corta dec(3, 4, 5)(marzo a mayo).TRUE ~ "Otoño"— la última línea es el “para todo lo demás”. ComoTRUEsiempre se cumple, atrapa cualquier caso que no haya calzado antes (los meses 9, 10, 11).
En `case_when()`, el orden importa
case_when() evalúa las condiciones de arriba hacia abajo y se queda con la primera que se cumple. Por eso el TRUE ~ ... va siempre al final: si lo pusieras primero, atraparía todo y las demás condiciones nunca se evaluarían. Piensa en las condiciones como un colador: de la más específica a la más general.
Resumir por año y estación
Queremos un valor por cada combinación de año y estación: la mediana de minutos fuera. Aquí entra el dúo group_by() + summarise(), una de las parejas más usadas de dplyr:
resumen <- sessions |> group_by(anio, estacion) |> summarise(mediana = median(duracion_min), .groups = "drop")group_by(anio, estacion)— divide la tabla en grupos, uno por cada combinación de año y estación.summarise(mediana = median(duracion_min))— para cada grupo, calcula la mediana y la guarda en una columna nueva,mediana. Usamos la mediana (y no el promedio) por lo que vimos en el módulo 8: resiste mejor las salidas extremas..groups = "drop"— desagrupa la tabla al terminar. Si lo omites, dplyr te deja la tabla agrupada y te muestra un mensaje algo confuso. Ponerlo es una buena costumbre.
`group_by` + `summarise`: el patrón de oro para resumir
Esta combinación responde la pregunta “¿cuál es un número (promedio, mediana, total, conteo…) por cada grupo?”. La vas a usar en casi todos tus análisis. group_by decide los grupos; summarise calcula el número de cada uno.
Varias líneas en un gráfico con color
Ahora dibujamos una línea por estación, todas en el mismo gráfico, distinguidas por color:
library(ggplot2)
resumen |> ggplot(aes(x = anio, y = mediana, color = estacion)) + geom_line(linewidth = 1) + geom_point() + labs( title = "Mediana de minutos fuera por estación y año", x = "Año", y = "Minutos fuera (mediana)", color = "Estación" ) + theme_minimal()La clave está en color = estacion dentro de aes(): le decimos a ggplot2 “dibuja una línea distinta para cada estación y dales colores diferentes”. ggplot2 separa los datos por estación, dibuja una línea para cada una y agrega la leyenda automáticamente.
geom_line(linewidth = 1)— las líneas, un poco más gruesas (linewidthcontrola el grosor).geom_point()— puntos sobre cada año, para marcar los valores reales.
`color` vs `fill` en líneas
Para líneas y puntos se usa color (el color del trazo). fill (relleno) es para formas con área, como barras, cajas o densidades. Si una línea no cambia de color al usar fill = estacion, es porque usaste la propiedad equivocada: las líneas se pintan con color, no con fill.
Error: todas mis líneas se conectan en una sola maraña
Si en vez de una línea por grupo ves una sola línea saltarina, es que ggplot2 no supo cómo agrupar. Mapear color = estacion normalmente basta (define los grupos solo). Si dibujas una sola línea de un color y aun así zigzaguea, revisa que los datos estén ordenados por el eje X (arrange(anio)).
Toques opcionales: etiquetar cada curva al final
Una leyenda obliga al lector a “ir y volver” entre las líneas y el recuadro de colores. Una alternativa más elegante es escribir el nombre de cada estación al final de su línea y quitar la leyenda. Lo hacemos en tres pasos.
Paso 1 — quedarnos con el último punto de cada estación. Para colocar la etiqueta al final, necesitamos saber dónde termina cada línea (su último año):
ultimos <- resumen |> group_by(estacion) |> filter(anio == max(anio)) |> ungroup()group_by(estacion) agrupa por estación y filter(anio == max(anio)) deja, dentro de cada grupo, solo la fila del año más reciente. El resultado, ultimos, tiene una fila por estación: justo donde irá cada etiqueta.
Paso 2 — dibujar las etiquetas y quitar la leyenda:
resumen |> ggplot(aes(x = anio, y = mediana, color = estacion)) + geom_line(linewidth = 1) + geom_point() + geom_text( data = ultimos, aes(label = estacion), hjust = 0, nudge_x = 0.2, fontface = "bold", show.legend = FALSE ) + scale_x_continuous(expand = expansion(mult = c(0.02, 0.15))) + labs(title = "Mediana de minutos fuera por estación y año", x = "Año", y = "Minutos fuera (mediana)") + theme_minimal() + theme(legend.position = "none")Lo nuevo, línea por línea:
geom_text(data = ultimos, ...)— dibuja texto usando otra tabla (ultimos), no la delggplot(). Esto es muy útil: cada capa puede tener sus propios datos. Comoultimostambién tienecolor = estacion, cada etiqueta sale del color de su línea.hjust = 0, nudge_x = 0.2— alinea el texto a la izquierda y lo empuja un poco a la derecha, para que quede después del último punto y no encima.scale_x_continuous(expand = expansion(mult = c(0.02, 0.15)))— agranda el margen derecho del eje (un 15%) para que quepan las etiquetas. Sin esto, los nombres se cortan en el borde.theme(legend.position = "none")— quita la leyenda: ya no hace falta, porque cada línea está rotulada.
Un detalle con estos datos: en el último año las cuatro estaciones terminan muy cerca (todas con valores bajos, porque 2024 está incompleto), así que con geom_text los nombres se encimarían y se cortarían. La solución es ggrepel, que separa las etiquetas automáticamente.
Paso 3 — separar las etiquetas con ggrepel:
# pak::pak("ggrepel") # instalar una vezlibrary(ggrepel)
resumen |> ggplot(aes(x = anio, y = mediana, color = estacion)) + geom_line(linewidth = 1) + geom_point() + geom_text_repel( data = ultimos, aes(label = estacion), hjust = 0, nudge_x = 0.8, direction = "y", box.padding = 0.5, min.segment.length = 0, segment.color = "grey70", segment.size = 0.3, max.overlaps = Inf, fontface = "bold", show.legend = FALSE ) + scale_x_continuous(expand = expansion(mult = c(0.02, 0.28))) + coord_cartesian(clip = "off") + labs(title = "Mediana de minutos fuera por estación y año", x = "Año", y = "Minutos fuera (mediana)") + theme_minimal() + theme(legend.position = "none", plot.margin = margin(6, 20, 6, 6))Detalle por detalle, lo que hace que no se encimen:
nudge_x = 0.8+direction = "y"— empuja los nombres bien a la derecha y los separa solo en vertical.box.padding = 0.5— exige más espacio alrededor de cada texto, así Otoño e Invierno (que en 2024 quedan casi a la misma altura) se apartan.min.segment.length = 0+segment.color = "grey70"— dibuja una línea-guía gris desde cada nombre hasta su punto, para que se entienda cuál es cuál aunque queden cerca.coord_cartesian(clip = "off")+ másexpandyplot.margin— permiten que las etiquetas se dibujen en el margen derecho sin cortarse.
Otro extra: colores con sentido
Por defecto ggplot2 asigna colores automáticos. Puedes elegirlos tú con scale_color_manual() para que cada estación tenga un color “lógico”:
scale_color_manual(values = c( Invierno = "#4F9DDE", # azul frío Primavera = "#3FAE5A", # verde Verano = "#E4572E", # naranja cálido Otoño = "#E0A106" # ámbar))Agrégalo como una capa más (+ scale_color_manual(...)). Es un detalle pequeño que hace el gráfico mucho más intuitivo.
Ejercicio
Ejercicio 10 — Tendencias en el tiempo
- Haz la línea de tendencia general con
geom_smooth()yscale_y_log10(). - Crea las columnas
anioyestacionconcase_when(). - Resume la mediana de
duracion_minpor año y estación congroup_by+summarise. - Dibuja una línea por estación usando
color = estacion. - Extra: cambia
geom_smooth()porgeom_smooth(method = "lm")en el primer gráfico. ¿La tendencia general sube o baja a lo largo de los años?
Ver solución
library(dplyr)library(lubridate)library(ggplot2)
# 1. Tendencia generalggplot(sessions, aes(x = salida, y = duracion_min)) + geom_smooth() + scale_y_log10() + labs(title = "Tendencia del tiempo fuera (2011–2024)", x = "Fecha", y = "Minutos fuera (log)") + theme_minimal()
# 2–4. Estacionesresumen <- sessions |> mutate( anio = year(salida), estacion = case_when( month(salida) %in% c(12, 1, 2) ~ "Invierno", month(salida) %in% 3:5 ~ "Primavera", month(salida) %in% 6:8 ~ "Verano", TRUE ~ "Otoño" ) ) |> group_by(anio, estacion) |> summarise(mediana = median(duracion_min), .groups = "drop")
resumen |> ggplot(aes(x = anio, y = mediana, color = estacion)) + geom_line(linewidth = 1) + geom_point() + labs(title = "Mediana de minutos fuera por estación y año", x = "Año", y = "Minutos fuera (mediana)", color = "Estación") + theme_minimal()
# 5. Tendencia linealggplot(sessions, aes(x = salida, y = duracion_min)) + geom_smooth(method = "lm") + scale_y_log10() + theme_minimal()Sobre el extra: la tendencia suele mostrar que el verano es la estación de paseos más largos (más luz, mejor clima = más aventura felina), mientras que el invierno es la de paseos más cortos. Tiene todo el sentido del mundo para un gato. La recta general (method = "lm") te dará la dirección de fondo a lo largo de los 14 años.
El gran final: el mapa de calor
El objetivo del módulo
Llegamos al gran final. En este módulo construimos el gráfico que prometimos en el módulo 1: el mapa de calor que resume 14 años de la vida de Pépito en un solo cuadro. Lo haremos en dos pasos: primero un mapa de calor sencillo para aprender la técnica (geom_tile), y después la versión completa y pulida, capa por capa.
¿Qué es un mapa de calor?
Un mapa de calor (heatmap) es una cuadrícula donde cada celda representa la combinación de dos categorías (por ejemplo, un día y una hora), y el color de la celda codifica un valor numérico. Es ideal para responder “¿cuándo / dónde se concentra algo?” cuando tienes dos dimensiones a la vez.
El geom que dibuja celdas se llama geom_tile(): necesita un x, un y y un fill (el color que codifica el valor).
Calentamiento: ¿cuándo está activo Pépito?
Empecemos con un mapa de calor de día de la semana × hora, donde el color sea la cantidad de eventos. Primero contamos:
library(dplyr)
heat <- pepito_df |> count(weekday, hour)count(weekday, hour) cuenta cuántos eventos hay para cada combinación de día de la semana y hora. El resultado tiene tres columnas: weekday, hour y n (el conteo). Ahora lo graficamos:
library(ggplot2)
ggplot(heat, aes(x = hour, y = weekday, fill = n)) + geom_tile() + scale_fill_viridis_c() + scale_x_continuous(breaks = 0:23) + labs( title = "¿Cuándo está activo Pépito?", x = "Hora del día", y = "Día de la semana", fill = "Eventos" ) + theme_minimal()Las piezas nuevas:
aes(x = hour, y = weekday, fill = n)— dos dimensiones en los ejes (hora y día) y el conteonen el color (fill).geom_tile()— dibuja un rectángulo por cada combinación, pintado segúnfill.scale_fill_viridis_c()— la escala de color viridis, que va de morado oscuro a amarillo. Es la estrella de los mapas de calor (ya verás por qué). Lacfinal es de continuous, porquenes un número continuo.
¿Por qué viridis y no un degradado cualquiera?
La escala viridis está diseñada para que las diferencias de color se perciban de forma uniforme y para que siga siendo legible en blanco y negro y para personas con daltonismo. Por eso es la opción profesional por defecto en mapas de calor. Está incluida en ggplot2 moderno, así que no necesitas instalar nada extra: solo scale_fill_viridis_c().
Detente y piensa
Mira el patrón: las celdas amarillas (más eventos) se concentran en ciertas horas del día, bastante parecidas de lunes a domingo. ¿Hay alguna diferencia entre los días de semana y el fin de semana? Acabas de leer el “horario laboral” de un gato. 🐱
El gráfico estrella: minutos fuera por año y mes
Ahora el gran final. Queremos una celda por cada mes de cada año, coloreada según la mediana de minutos que Pépito pasó fuera. Primero el resumen, con las herramientas del módulo 10:
library(dplyr)library(lubridate)
datos_heat <- sessions |> mutate( anio = year(salida), mes = month(salida, label = TRUE) ) |> group_by(anio, mes) |> summarise(mediana = median(duracion_min), .groups = "drop")month(salida, label = TRUE)— extrae el mes con su nombre (ene,feb…) en vez de un número, y como factor ordenado (para que enero vaya antes que febrero en el eje).group_by(anio, mes)+summarise(mediana = median(...))— la mediana de minutos fuera para cada combinación año-mes.
Ahora la versión sencilla del gráfico:
library(ggplot2)
ggplot(datos_heat, aes(x = mes, y = factor(anio), fill = mediana)) + geom_tile() + scale_fill_viridis_c() + labs(title = "Mediana de minutos fuera por año y mes", x = "Mes", y = "Año", fill = "Min.") + theme_minimal()Fíjate en y = factor(anio): como en los boxplots, convertimos el año en categoría para tener una fila por año. Ya tienes un mapa de calor funcional. Ahora vamos a pulirlo hasta dejarlo de calidad de publicación.
De lo sencillo a lo profesional, una capa a la vez
La versión final que viste en el módulo 1 parece intimidante, pero no es más que el gráfico de arriba con capas añadidas de a una. Vamos a construirla así, paso a paso, agregando una sola cosa cada vez y mirando el resultado. Parte siempre del gráfico sencillo de la sección anterior.
Paso 1 — Separar las celdas con un borde blanco
Pegadas unas a otras, las celdas se confunden. Un borde blanco las separa:
ggplot(datos_heat, aes(x = mes, y = factor(anio), fill = mediana)) + geom_tile(color = "white", linewidth = 0.4) + scale_fill_viridis_c() + theme_minimal()Lo único que cambió es geom_tile(color = "white", linewidth = 0.4). Cuidado con un detalle que confunde mucho: en geom_tile, fill es el relleno de la celda (el color del dato) y color es el borde. linewidth controla el grosor de ese borde. Mira el resultado: ahora se ve una cuadrícula limpia.
Paso 2 — Escribir el número dentro de cada celda
El color ya dice “mucho o poco”, pero un número exacto ayuda. Sumamos una capa de texto:
ggplot(datos_heat, aes(x = mes, y = factor(anio), fill = mediana)) + geom_tile(color = "white", linewidth = 0.4) + geom_text(aes(label = round(mediana)), color = "white", size = 3.2, fontface = "bold") + scale_fill_viridis_c() + theme_minimal()La capa nueva es geom_text(...):
aes(label = round(mediana))— el texto de cada celda es el valor demediana, redondeado conround()(para no mostrar decimales feos). Va dentro deaes()porque el texto depende de los datos.color = "white", size = 3.2, fontface = "bold"— el color, tamaño y grosor del texto. Van fuera deaes()porque son fijos para todos.
Acabas de poner una capa de texto encima de las celdas. Recuerda: el orden importa, geom_text va después de geom_tile para quedar arriba.
Paso 3 — Títulos claros con labs()
Un buen gráfico se explica solo. Agregamos título, subtítulo y nombres de ejes:
# ...las dos capas anteriores, y además: labs( title = "¿Cuánto tiempo pasa Pépito fuera?", subtitle = "Mediana de minutos fuera por año y mes (2011–2024)", x = "Mes", y = "Año", fill = "Minutos" ) +Lo nuevo frente a lo que ya conoces es subtitle: una línea secundaria bajo el título, ideal para aclarar qué se está midiendo.
Paso 4 — Agrandar todo el texto de un saque
El texto por defecto se ve pequeño. En vez de ajustarlo elemento por elemento, hay un atajo: pasarle un tamaño base al tema.
theme_minimal(base_size = 15)base_size = 15 sube de golpe el tamaño de todo el texto del gráfico (ejes, leyenda, etc.). Es el truco más rápido para que un gráfico se vea mejor sin tocar nada más.
Paso 5 — Quitar la rejilla y agrandar la leyenda
En un mapa de calor, la rejilla de fondo sobra (las celdas ya son la cuadrícula). Y la barrita de color de la leyenda se agradece más alta. Para esos retoques finos usamos theme() (en minúscula):
theme_minimal(base_size = 15) + theme( panel.grid = element_blank(), # quita la rejilla de fondo legend.key.height = unit(1.2, "cm") # barra de color más alta )panel.grid = element_blank()—element_blank()significa “no dibujes esto”. Aquí, elimina la rejilla.legend.key.height = unit(1.2, "cm")— hace más alta la barra de color (necesitaunit()para indicar la medida).
`theme_*()` vs `theme()` — no son lo mismo
theme_minimal(),theme_bw(),theme_classic()… son temas completos prearmados: cambian todo el aspecto de una sola vez. Empieza siempre por uno de estos.theme(...)(en minúscula, sin sufijo) es el ajuste fino: modifica elementos concretos (la rejilla, la leyenda, un título…).
El orden importa: primero el tema completo, luego los retoques con theme(), porque el segundo pisa al primero.
Paso 6 — Todo junto
Ahora sí, juntando los seis pasos obtienes la versión final. Cada línea ya la viste por separado; lo único extra son unos ajustes de tamaño y color de cada texto dentro de theme(), que son “más de lo mismo” del paso 5:
library(dplyr)library(ggplot2)library(lubridate)
datos_heat <- sessions |> mutate(anio = year(salida), mes = month(salida, label = TRUE)) |> group_by(anio, mes) |> summarise(mediana = median(duracion_min), .groups = "drop")
ggplot(datos_heat, aes(x = mes, y = factor(anio), fill = mediana)) + geom_tile(color = "white", linewidth = 0.4) + # paso 1 geom_text(aes(label = round(mediana)), # paso 2 color = "white", size = 3.2, fontface = "bold") + scale_fill_viridis_c(option = "viridis") + labs( # paso 3 title = "¿Cuánto tiempo pasa Pépito fuera?", subtitle = "Mediana de minutos fuera por año y mes (2011–2024)", x = "Mes", y = "Año", fill = "Minutos" ) + theme_minimal(base_size = 15) + # paso 4 theme( # paso 5 (+ retoques) plot.title = element_text(size = 20, face = "bold"), plot.subtitle = element_text(size = 14, color = "grey30", margin = margin(b = 12)), axis.title = element_text(size = 14, face = "bold"), axis.text = element_text(size = 12), legend.title = element_text(size = 13, face = "bold"), legend.text = element_text(size = 11), legend.key.height = unit(1.2, "cm"), panel.grid = element_blank() )Los element_text(size = ..., face = ...) dentro de theme() solo ajustan el tamaño y el estilo de cada texto (el título más grande, el subtítulo gris, etc.). Son opcionales: el gráfico ya funcionaba en el paso 5. Aquí están solo para dejarlo de calidad de publicación.
Lee tu obra
¡Lo lograste! Tómate un momento para leer el gráfico que construiste. Busca la celda más amarilla: vas a encontrar julio de 2015 con una mediana de 252 minutos — más de 4 horas fuera de casa, el récord de aventura de Pépito. Mira también si los veranos (meses centrales) tienden a ser más claros que los inviernos. Catorce años de la vida de un gato, resumidos en un cuadro que hiciste tú, capa por capa.
Errores comunes en mapas de calor
Error: celdas en blanco (huecos en la cuadrícula)
Si un año-mes no tiene datos (Pépito no registró salidas ese mes), no habrá celda y quedará un hueco. Es normal y honesto: refleja que no hay información. Si prefieres mostrarlo explícitamente, puedes completar las combinaciones faltantes con tidyr::complete(), pero para empezar, dejar el hueco está bien.
Error: `scale_fill_viridis_c` no existe / no se aplica el color
Dos causas: usar scale_color_viridis_c() (con color) cuando mapeaste fill — deben coincidir: si pintas con fill, la escala es scale_fill_*. O tener una versión muy antigua de ggplot2. Solución: asegúrate de mapear fill = mediana y usar scale_fill_viridis_c(), y de tener ggplot2 actualizado con pak::pak("ggplot2").
¿Texto negro o blanco dentro de las celdas?
El texto blanco se lee bien sobre las celdas oscuras (moradas), pero puede perderse sobre las amarillas claras. Para gráficos de máxima calidad, algunos hacen que el color del texto cambie según el fondo. No es necesario ahora, pero si tus números claros no se leen, prueba color = "grey20" en el geom_text, o ajusta el tamaño.
Ejercicio
Ejercicio 11 — Construye el gran final
- Haz el mapa de calor de calentamiento (
weekday×hour, color = conteo). - Crea la tabla
datos_heat(mediana deduracion_minpor año y mes). - Haz la versión sencilla del mapa de calor año × mes.
- Conviértela en la versión completa: agrega
geom_textcon los números, bordes blancos, subtítulo y los retoques detheme(). - Encuentra la celda más amarilla. ¿Qué mes y año fue el récord de Pépito?
Ver solución
library(dplyr)library(lubridate)library(ggplot2)
# 1. Calentamiento: weekday × hourheat <- pepito_df |> count(weekday, hour)
ggplot(heat, aes(x = hour, y = weekday, fill = n)) + geom_tile() + scale_fill_viridis_c() + scale_x_continuous(breaks = 0:23) + labs(title = "¿Cuándo está activo Pépito?", x = "Hora del día", y = "Día de la semana", fill = "Eventos") + theme_minimal()
# 2. Resumen año × mesdatos_heat <- sessions |> mutate(anio = year(salida), mes = month(salida, label = TRUE)) |> group_by(anio, mes) |> summarise(mediana = median(duracion_min), .groups = "drop")
# 3 y 4. El gran finalggplot(datos_heat, aes(x = mes, y = factor(anio), fill = mediana)) + geom_tile(color = "white", linewidth = 0.4) + geom_text(aes(label = round(mediana)), color = "white", size = 3.2, fontface = "bold") + scale_fill_viridis_c(option = "viridis") + labs(title = "¿Cuánto tiempo pasa Pépito fuera?", subtitle = "Mediana de minutos fuera por año y mes (2011–2024)", x = "Mes", y = "Año", fill = "Minutos") + theme_minimal(base_size = 15) + theme( plot.title = element_text(size = 20, face = "bold"), plot.subtitle = element_text(size = 14, color = "grey30", margin = margin(b = 12)), axis.title = element_text(size = 14, face = "bold"), axis.text = element_text(size = 12), legend.title = element_text(size = 13, face = "bold"), legend.text = element_text(size = 11), legend.key.height = unit(1.2, "cm"), panel.grid = element_blank() )El récord: la celda más amarilla es julio de 2015, con una mediana de 252 minutos (más de 4 horas) fuera de casa. Si lo encontraste, ¡felicitaciones! Construiste, desde un JSON crudo hasta una visualización de calidad de publicación, tú solo. En el último módulo aprenderás a pulir y guardar tus gráficos para compartirlos.
Pulir y compartir: temas, etiquetas y ggsave
El objetivo del módulo
Ya sabes construir gráficos. Este último módulo es tu caja de herramientas para pulirlos y compartirlos: cambiar temas y colores, dividir un gráfico en pequeños múltiplos, ajustar la leyenda y los ejes, y —fundamental— guardar tu gráfico como imagen para un informe o una presentación. Cierra con un recetario y los errores más comunes de todo el curso.
Recordatorio: las capas que ya dominas
Antes de pulir, recordemos el esqueleto. Cualquier gráfico de este curso es esto, con más o menos capas:
ggplot(datos, aes(x = ..., y = ..., fill/color = ...)) + # datos + mapeo geom_algo() + # la forma scale_...() + # escalas (opcional) labs(...) + # etiquetas theme_...() # aparienciaPulir un gráfico es, casi siempre, trabajar en las tres últimas capas.
Temas: cambia el aspecto de una sola vez
Un tema completo cambia todo el look del gráfico. Prueba varios sobre el mismo gráfico y quédate con el que más te guste:
library(ggplot2)
base <- ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "dodge")
base + theme_minimal() # limpio, fondo blanco, rejilla suave (el del curso)base + theme_bw() # blanco y negro, con marcobase + theme_classic() # estilo clásico, sin rejillabase + theme_void() # sin ejes ni fondo (útil para mapas de calor o arte)Guarda tu gráfico base en una variable
Como ves arriba, puedes guardar un gráfico en una variable (base <- ggplot(...) + ...) y luego sumarle capas sobre la marcha (base + theme_bw()). Es comodísimo para probar variantes sin reescribir todo. Un gráfico de ggplot2 es un objeto de R como cualquier otro.
Colores a tu gusto
Para elegir los colores de los grupos, usa una escala scale_..._manual() (colores que tú eliges) o una paleta prearmada:
# Colores elegidos a manobase + scale_fill_manual(values = c("in" = "#2C7FB8", "out" = "#F03B20")) + theme_minimal()
# Una paleta prearmada (ColorBrewer)base + scale_fill_brewer(palette = "Set2") + theme_minimal()scale_fill_manual(values = c(...))— asignas un color a cada categoría. Puedes usar nombres ("red") o códigos hexadecimales ("#2C7FB8").scale_fill_brewer(palette = "Set2")— usa una paleta diseñada por expertos en color. Hay muchas ("Set1","Dark2","Paired"…).
`fill` o `color`: que la escala coincida
Si mapeaste el grupo con fill, usa scale_fill_*. Si lo mapeaste con color (líneas, puntos), usa scale_color_*. Mezclarlos es un error silencioso muy común: cambias la escala y “no pasa nada”, porque estás tocando una propiedad que no usaste.
Pequeños múltiplos con facet_wrap()
Una de las herramientas más potentes de ggplot2: facet_wrap() parte un gráfico en varios mini-gráficos, uno por cada valor de una variable. Por ejemplo, la actividad por hora, un panel por día de la semana:
ggplot(pepito_df, aes(x = hour)) + geom_bar(fill = "steelblue") + facet_wrap(~ weekday) + scale_x_continuous(breaks = seq(0, 24, 6)) + labs(title = "Actividad por hora, según el día de la semana", x = "Hora", y = "Eventos") + theme_minimal()facet_wrap(~ weekday)— crea un panel por cada día. La~(virgulilla) se lee “según”: divide segúnweekday.
Comparar paneles lado a lado suele revelar diferencias que un solo gráfico esconde. Es ideal para “¿es distinto el patrón entre grupos?”.
`facet_wrap` vs `facet_grid`
facet_wrap(~ var)— paneles por una variable, acomodados en una grilla automática.facet_grid(fila ~ columna)— paneles en una matriz por dos variables (una en filas, otra en columnas).
Empieza con facet_wrap; pasa a facet_grid cuando quieras cruzar dos variables.
Ajustes finos frecuentes
Dos retoques que vas a necesitar seguido, ambos con theme():
base + theme_minimal() + theme( legend.position = "top", # mueve la leyenda arriba axis.text.x = element_text(angle = 45, hjust = 1) # rota las etiquetas del eje X )legend.position—"top","bottom","left","right"o"none"(para ocultarla).axis.text.x = element_text(angle = 45, hjust = 1)— gira las etiquetas del eje X 45°, útil cuando son largas y se enciman.hjust = 1las alinea bien.
Lo más importante: guardar tu gráfico con ggsave()
Un gráfico que solo vive en la pantalla de R no sirve para un informe. ggsave() lo guarda como archivo de imagen:
mi_grafico <- ggplot(pepito_df, aes(x = hour, fill = way)) + geom_bar(position = "dodge") + theme_minimal()
ggsave("pepito_horas.png", plot = mi_grafico, width = 8, height = 5, dpi = 300)"pepito_horas.png"— el nombre del archivo. La extensión decide el formato:.png(imagen normal),.pdf(vectorial, escala sin pixelarse),.jpg,.svg…plot = mi_grafico— qué gráfico guardar. Si lo omites,ggsave()guarda el último gráfico que dibujaste.widthyheight— el tamaño en pulgadas.dpi = 300— la resolución; 300 es calidad de impresión.
Errores típicos al guardar
Error: Cannot find the file / could not open— la carpeta de destino no existe.ggsave()no crea carpetas; créala antes o guarda en una que exista.- La imagen sale en blanco — guardaste antes de dibujar, o pasaste el objeto equivocado en
plot =. Asegúrate de quemi_graficocontenga el gráfico. - El texto sale gigante o minúsculo — el tamaño del texto depende de
width/height. Si las letras se ven mal, ajusta esas dimensiones (un gráfico más grande “achica” el texto relativo).
PNG para la web, PDF para imprimir
Para una presentación o la web, usa .png con dpi = 300. Para un documento que se va a imprimir o que necesita verse nítido a cualquier tamaño, usa .pdf (es vectorial: nunca se pixela). Cambiar de formato es tan simple como cambiar la extensión del nombre.
Recetario: ¿qué geom para qué pregunta?
| Tu pregunta | Geom | Visto en |
|---|---|---|
| ¿Cuántos hay de cada categoría? | geom_bar() | Módulo 6 |
| Ya tengo los totales, dibújalos | geom_col() | Módulo 6 |
| ¿Cómo cambia algo en el tiempo? | geom_line(), geom_freqpoly() | Módulo 7 |
| ¿Cómo se distribuye una variable? | geom_histogram(), geom_density() | Módulo 8 |
| ¿Cómo comparo distribuciones entre grupos? | geom_boxplot() | Módulo 9 |
| ¿Cuál es la tendencia entre tanto punto? | geom_smooth() | Módulo 10 |
| ¿Dónde se concentra algo en 2 dimensiones? | geom_tile() | Módulo 11 |
| ¿Quiero escribir valores en el gráfico? | geom_text() | Módulo 11 |
| ¿Quiero un panel por grupo? | facet_wrap() | Módulo 12 |
Los errores que más se repiten (resumen del curso)
Los 7 tropiezos clásicos de ggplot2
- El
+al principio de la línea en vez de al final. → Va al final. - Color fijo dentro de
aes()(aes(fill = "blue")). → Color fijo va fuera deaes(). - Mezclar
|>y+. →|>para datos,+para capas de ggplot2. - Olvidar
factor()en boxplots y mapas de calor con ejes numéricos. → Una sola caja gigante es la señal. scale_fill_*vsscale_color_*que no coinciden con lo que mapeaste.- Fechas que son texto. → Conviértelas con
lubridate(módulo 4); compruébalo conglimpse(). - Usar
lead()/lag()sinarrange()antes. → Ordena siempre primero.
Hasta aquí llegamos (¡felicitaciones!)
Empezaste sin saber qué era un JSON y terminaste construyendo un mapa de calor de calidad de publicación con 14 años de datos. Por el camino aprendiste a:
- Leer un JSON desde la web y convertirlo en un data frame.
- Transformar fechas de texto en fechas reales y crear columnas útiles.
- Entender la gramática de ggplot2 y construir gráficos por capas.
- Elegir el gráfico correcto según la pregunta.
- Pulir, colorear, dividir en paneles y guardar tus gráficos.
Lo más valioso que te llevas no es una lista de funciones, sino una forma de pensar: datos → mapeo → geometría → pulido. Con eso puedes graficar cualquier dataset, no solo el de Pépito.
¿Hacia dónde seguir?
- Aplica lo aprendido a tus propios datos: el flujo es siempre el mismo.
- Explora la galería oficial de ggplot2 y la cheat sheet de RStudio para descubrir más geoms.
- Mira
patchwork(para combinar varios gráficos en una figura) yplotly/ggiraph(para gráficos interactivos). - Revisa los otros cursos de Hazla con Datos (al pie de la página).
Ejercicio 12 — Tu propia visualización
El ejercicio final no tiene solución única. Toma una de las tres preguntas que anotaste en el módulo 1 y respóndela con un gráfico, de principio a fin:
- Elige la pregunta y decide qué geom le corresponde (usa el recetario).
- Prepara los datos que necesites (
filter,mutate,group_by+summarise…). - Construye el gráfico capa por capa.
- Púlelo: título, colores, tema.
- Guárdalo con
ggsave()y compártelo (¡con la comunidad de Hazla con Datos, si quieres!).
Si llegaste hasta aquí y puedes hacer esto sin copiar, ya sabes ggplot2. 🎉
Ver un ejemplo resuelto
Pregunta de ejemplo: “¿A qué hora suele volver Pépito a casa?”
library(dplyr)library(ggplot2)
# 1–2. Preparar: solo las entradas, con su horaentradas <- pepito_df |> filter(way == "in") |> mutate(hora = hour(datetime_fr))
# 3–4. Graficar y pulirg <- ggplot(entradas, aes(x = hora)) + geom_bar(fill = "#2C7FB8") + scale_x_continuous(breaks = 0:23) + labs(title = "¿A qué hora vuelve Pépito a casa?", x = "Hora del día", y = "Número de entradas") + theme_minimal()
g
# 5. Guardarggsave("pepito_regreso.png", plot = g, width = 8, height = 5, dpi = 300)Lo importante no es este gráfico en particular, sino que recorriste el flujo completo —preparar, graficar, pulir, guardar— por tu cuenta. Esa es exactamente la habilidad que te llevas del curso.