IRdisplay::display_html('
<style>
@import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;600&family=Amiri:wght@400;700&display=swap");
.rendered_html, .markdown, .cell .text_cell_render { font-family:"Work Sans",system-ui,sans-serif; color:#122535; }
.rendered_html h1,.rendered_html h2,.rendered_html h3 { font-family:"Amiri",Georgia,serif; color:#00529B; }
.rendered_html h2 { border-bottom:2px solid #00529B; padding-bottom:.2em; }
.rendered_html a { color:#00529B; }
.rendered_html table th { background:#00529B; color:#fff; }
.rendered_html h1,.rendered_html h2,.rendered_html h3 { scroll-margin-top:16px; }
</style>
')Clase 3 · Visualización de datos
Analítica de Datos · Maestría en Ciencias del Comportamiento · Universidad de San Andrés
2do Cuatrimestre 2026 · 22/08/2026
Lenguaje: R. Esta notebook hace exactamente lo mismo que la de Python: los mismos datos, los mismos pasos, los mismos números y las mismas figuras. Lo que cambia es la herramienta: acá graficamos con ggplot2, que es la forma natural de decir estas cosas en R.
Si se van a llevar una única idea de la clase de hoy, debe ser la siguiente:
Un gráfico es un mapeo de variables (datos) a atributos gráficos, y de esto depende qué historia se puede contar.
Cómo se usa esta notebook. Las celdas de código se corren con
Shift+Enter, de arriba hacia abajo. Si te salteás una, las de abajo pueden fallar porque dependen de variables definidas antes.
La bibliografía de hoy
Toda la clase sale de un solo libro, de acceso abierto y gratuito:
Wilke, C. O. (2019). Fundamentals of Data Visualization. O’Reilly. Libro web completo, en inglés: clauswilke.com/dataviz
No hay que leerlo entero. Cada sección de esta notebook sale de un capítulo, y son cortos:
| Sección de hoy | Capítulos de Wilke |
|---|---|
| 1. El criterio | 1. Introduction — de ahí sale feo / malo / incorrecto |
| 2. El mapeo | 2. Mapping data onto aesthetics |
| 3. El repertorio | 5. Directory of visualizations, 6. Visualizing amounts, 7. Histograms and density plots, 9. Visualizing many distributions at once |
| 4. Las decisiones | 3. Coordinate systems and axes, 4. Color scales, 19. Common pitfalls of color use, 26. Don’t go 3D |
| 5. Componer y comunicar | 21. Multi-panel figures, 22. Titles, captions, and tables |
| 6. El informe | 29. Telling a story and making a point |
Las dos figuras del libro que aparecen más abajo están reproducidas con atribución, bajo su licencia CC BY-NC-ND 4.0.
Del lado de R, para la sintaxis de ggplot2, R para Ciencia de Datos (Wickham y Grolemund) está en castellano y también es gratis: cap. 3, Visualización de datos y cap. 28, Comunicar con gráficos.
Y dos catálogos para consultar mientras grafican (no para leer de corrido): la galería de ggplot2 y from Data to Viz.
La idea de la clase, en dos figuras
Antes de ponernos a trabajar, un ejemplo prestado del capítulo 2 de Wilke.
Los datos son las temperaturas diarias de cuatro ciudades de Estados Unidos: el promedio histórico de cada día del año, medido por el servicio meteorológico estadounidense (NOAA). Cada fila es un día en una ciudad, y hay cuatro columnas: la ciudad, el día del año, el mes y la temperatura.
Con esas cuatro columnas se arma la figura de abajo. Mirala un minuto antes de seguir leyendo, porque la vamos a desarmar entre todos.
#@title Los datos: temperaturas diarias de la NOAA {display-mode: "form"}
# Descarga y prepara los datos originales del libro.
# No hace falta leer este codigo: lo que importa es la tabla que sale abajo.
suppressPackageStartupMessages({
library(readr) # leer archivos
library(dplyr) # manipular tablas
library(tidyr) # reacomodar tablas
library(ggplot2) # graficar
})
# patchwork sirve para poner varias figuras en una misma hoja. Es lo unico que no
# viene con Colab; si falta, se instala solo (tarda unos segundos, una sola vez).
if (!requireNamespace("patchwork", quietly = TRUE)) install.packages("patchwork")
suppressPackageStartupMessages(library(patchwork))
# Sin esto las tablas del tidyverse salen con codigos de color que Colab imprime
# como basura (ESC[90m...) en vez de colorear.
options(crayon.enabled = FALSE)
options(repr.plot.width = 9, repr.plot.height = 4)
dir.create("figuras", showWarnings = FALSE)
# El corrector de las consignas de opcion multiple de esta notebook.
corregir <- function(...) {
for (r in list(...)) {
if (r[[2]] == "elegir") cat("⚪", r[[1]], "— todavía sin responder\n")
else if (r[[2]] == r[[3]]) cat("✅", r[[1]], "—", r[[2]], "\n")
else cat("❌", r[[1]], "—", r[[2]], " · la respuesta es:", r[[3]], "\n")
}
}
# Servicio publico de la NOAA: temperatura media normal (promedio 1981-2010) de cada
# dia del año, para las mismas cuatro estaciones que usa el libro.
URL_NOAA <- paste0("https://www.ncei.noaa.gov/access/services/data/v1",
"?dataset=normals-daily",
"&stations=USW00014819,USC00042319,USW00093107,USW00012918",
"&dataTypes=DLY-TAVG-NORMAL",
"&startDate=2010-01-01&endDate=2010-12-31",
"&format=csv&units=standard")
temperaturas <- read_csv(URL_NOAA, show_col_types = FALSE)
nombres_ciudad <- c(USW00014819 = "Chicago", USC00042319 = "Valle de la Muerte",
USW00093107 = "San Diego", USW00012918 = "Houston")
# La columna DATE trae "01-01", "01-02", ...: le pegamos un año bisiesto para poder
# leerla como fecha y sacar el dia del año y el mes.
# El libro las muestra en grados Fahrenheit; aca las pasamos a Celsius.
temperaturas <- temperaturas |>
mutate(ciudad = nombres_ciudad[STATION],
fecha = as.Date(paste0("2012-", DATE)),
dia_del_año = as.integer(format(fecha, "%j")),
mes = as.integer(format(fecha, "%m")),
temperatura = (`DLY-TAVG-NORMAL` - 32) * 5 / 9)
# Las cuatro columnas que importan hoy. Cada fila es un dia en una ciudad.
head(select(temperaturas, ciudad, dia_del_año, mes, temperatura))#@title Temperaturas normales diarias en cuatro ciudades (Wilke, cap. 2, fig. 2.3) {display-mode: "form"}
# Recreacion de la figura del libro con los datos de arriba.
# No hace falta leer este codigo: lo que importa es la figura.
orden_ciudades <- c("Valle de la Muerte", "Houston", "San Diego", "Chicago")
p <- ggplot(temperaturas,
aes(x = dia_del_año, y = temperatura,
colour = factor(ciudad, levels = orden_ciudades))) +
geom_line(linewidth = 0.7) +
labs(x = "día del año", y = "temperatura (°C)", colour = "ciudad",
title = "Temperaturas normales diarias en cuatro ciudades de EE.UU.")
ggsave("figuras/clase03_wilke_temperaturas_lineas_r.png", p,
dpi = 150, width = 9, height = 4, bg = "white")
p
Recreación de la figura 2.3 de Wilke (cap. 2) con los datos originales. Fuente: NOAA. El libro las muestra en grados Fahrenheit; acá están en Celsius.
Esa figura es un mapeo, y se escribe entero en tres renglones de código. Para entender esto, reemplazá abajo cada "elegir" por el atributo gráfico que corresponda, y volvé a correr la celda para ver si acertaste.
# ✏️ ¿Qué está mapeado a qué?
# Reemplazá cada "elegir" por la opción que corresponda y volvé a correr la celda.
# Opciones: "posición en el eje x", "posición en el eje y", "color", "tamaño", "forma"
# 1. La temperatura, ¿a qué atributo gráfico está mapeada?
temperatura <- "elegir"
# 2. ¿Y el día del año?
dia <- "elegir"
# 3. ¿Y la ciudad?
ciudad <- "elegir"
# 4. La escala que lleva la ciudad, ¿de qué tipo es? ("continua" o "discreta")
escala_ciudad <- "elegir"
corregir(list("1. temperatura", temperatura, "posición en el eje y"),
list("2. día del año", dia, "posición en el eje x"),
list("3. ciudad", ciudad, "color"),
list("4. escala de la ciudad", escala_ciudad, "discreta"))⚪ 1. temperatura — todavía sin responder
⚪ 2. día del año — todavía sin responder
⚪ 3. ciudad — todavía sin responder
⚪ 4. escala de la ciudad — todavía sin responder
No hay una cuarta cosa escondida: la figura es esas tres decisiones.
#@title Los mismos datos, otro mapeo (Wilke, cap. 2, fig. 2.4) {display-mode: "form"}
# Ahora la temperatura va al COLOR y la ciudad pasa a ser una posicion.
# Se promedia por mes para que cada cuadrado sea lo bastante grande como para
# que el color se pueda leer.
meses <- c("ene", "feb", "mar", "abr", "may", "jun",
"jul", "ago", "sep", "oct", "nov", "dic")
# De la mas fria a la mas calurosa. Ojo: en ggplot2 el primer nivel de un factor va
# ABAJO en el eje y, asi que la lista se da al reves para que quede como en el libro.
orden_calor <- c("Chicago", "San Diego", "Houston", "Valle de la Muerte")
promedios <- temperaturas |>
group_by(ciudad, mes) |>
summarise(temperatura = mean(temperatura), .groups = "drop") |>
mutate(ciudad = factor(ciudad, levels = rev(orden_calor)),
mes = factor(meses[mes], levels = meses))
p <- ggplot(promedios, aes(x = mes, y = ciudad, fill = temperatura)) +
geom_tile() +
scale_fill_viridis_c(option = "inferno", name = "temperatura (°C)") +
labs(x = "mes", y = "", title = "Temperatura normal media por mes y ciudad")
options(repr.plot.width = 9, repr.plot.height = 2.8)
ggsave("figuras/clase03_wilke_temperaturas_heatmap_r.png", p,
dpi = 150, width = 9, height = 2.8, bg = "white")
p
Recreación de la figura 2.4 de Wilke (cap. 2). Fuente: NOAA.
Son los mismos datos. Lo único que cambió es qué variable fue a qué atributo, así que el mapeo se puede cambiar. Las dos últimas preguntas tienen trampa.
# ✏️ El mismo dato, otro mapeo
# Mismas opciones que antes: "posición en el eje x", "posición en el eje y",
# "color", "tamaño", "forma". Para las escalas: "continua" o "discreta".
# 1. Acá, ¿a qué atributo gráfico está mapeada la temperatura?
temperatura <- "elegir"
# 2. ¿Y la ciudad?
ciudad <- "elegir"
# 3. El eje horizontal (los meses), ¿qué tipo de escala es?
eje_horizontal <- "elegir"
# 4. ¿Y el eje vertical (las ciudades)?
eje_vertical <- "elegir"
corregir(list("1. temperatura", temperatura, "color"),
list("2. ciudad", ciudad, "posición en el eje y"),
list("3. eje horizontal", eje_horizontal, "discreta"),
list("4. eje vertical", eje_vertical, "discreta"))⚪ 1. temperatura — todavía sin responder
⚪ 2. ciudad — todavía sin responder
⚪ 3. eje horizontal — todavía sin responder
⚪ 4. eje vertical — todavía sin responder
Las dos últimas son la trampa, y casi nadie las marca: en esta figura los dos ejes son escalas de posición discretas. El eje x no es el tiempo continuo, son doce categorías ordenadas; el eje y son cuatro ciudades, que ni siquiera tienen un orden natural. El orden en que las pusimos es una decisión nuestra, no un dato: “podría haber elegido cualquier otro orden y la figura habría sido igual de válida” (Wilke, cap. 2).
La idea con la que Wilke abre el capítulo 2 es la más importante de la clase:
“Todas las visualizaciones de datos mapean valores de los datos a características cuantificables del gráfico resultante. A esas características las llamamos atributos gráficos (aesthetics).” (Wilke, cap. 2)
Todo gráfico, por distinto que parezca un grafico de barras de un gráficos de torta, o de un heatmap, es la misma operación: mapear valores de los datos a atributos gráficos. El andamiaje completo son cuatro piezas:
Los atributos gráficos (aesthetics, el término del libro): posición, forma, tamaño, color, ancho de línea, tipo de línea. La lista es corta y cerrada.
Los tipos de dato: continuo (entre dos valores siempre hay uno intermedio), discreto (categórico sin orden, categórico ordenado)
La partición: posición, tamaño, color y ancho de línea pueden representar datos continuos; forma y tipo de línea, en general, solo discretos.
Las escalas: el diccionario que traduce cada valor de dato a un valor del atributo. La única regla dura del capítulo: la escala tiene que ser uno a uno, o sea que a cada valor del dato le corresponde exactamente un valor del atributo, y al revés. Si dos sedes comparten el mismo color, el gráfico no es feo ni malo: es incorrecto, porque quedó ambiguo.
Ojo que uno a uno no quiere decir lineal. Una escala logarítmica, una raíz cuadrada o un eje dado vuelta siguen siendo uno a uno: cada valor tiene su único lugar y desde el gráfico se puede volver al dato. Lo que se elige ahí es la forma de la traducción, y esa elección es libre siempre que quede declarada (el eje avisa que es logarítmico). Lo que el capítulo prohíbe es otra cosa: que dos valores distintos terminen en el mismo lugar.
Guardate la palabra aesthetics, porque en R la vas a tener delante de los ojos toda la clase: la función de ggplot2 que escribe el mapeo se llama, justamente, aes().
El inventario de atributos gráficos, dibujado:

Figura 2.1 de Claus O. Wilke, Fundamentals of Data Visualization (cap. 2), reproducida con atribución bajo licencia CC BY-NC-ND 4.0.
Una más difícil
Con el inventario a la vista, una figura más cargada, también del libro. Son 32 autos de 1973-74, y usa cinco escalas a la vez.
Antes de verla, el punto de partida: los mismos autos con dos variables, la cilindrada y la eficiencia del combustible. Dos escalas de posición y nada más. Este es el gráfico más común que existe.
#@title Los mismos 32 autos, con dos variables y nada más {display-mode: "form"}
# Los datos de la figura del libro (Motor Trend, 1974), desde su fuente publica.
# No hace falta leer este codigo: lo que importa es la figura.
autos <- read_csv("https://vincentarelbundock.github.io/Rdatasets/csv/datasets/mtcars.csv",
show_col_types = FALSE)
options(repr.plot.width = 6, repr.plot.height = 4)
ggplot(autos, aes(x = disp, y = mpg)) +
geom_point(size = 2.5) +
labs(x = "cilindrada (displacement, pulgadas cúbicas)",
y = "eficiencia del combustible (mpg)",
title = "32 autos de 1973-74: dos variables, dos escalas de posición")Ahora la misma figura, con tres variables más encima. No cambia el tipo de gráfico: siguen siendo los mismos 32 puntos en los mismos dos ejes.
Mirala y contestá, para cada variable: ¿a qué atributo gráfico está mapeada, y esa escala es continua o discreta?

Figura 2.5 de Claus O. Wilke, Fundamentals of Data Visualization (cap. 2), reproducida con atribución bajo licencia CC BY-NC-ND 4.0. Fuente de los datos: Motor Trend, 1974.
# ✏️ Las cinco escalas de la figura 2.5
# Mismas opciones de siempre: "posición en el eje x", "posición en el eje y",
# "color", "tamaño", "forma".
# 1. La potencia (power), ¿a qué atributo está mapeada?
potencia <- "elegir"
# 2. ¿Y el peso (weight)?
peso <- "elegir"
# 3. ¿Y los cilindros (cylinders)?
cilindros <- "elegir"
# 4. De las cinco variables, ¿cuál es la única que NO es continua?
# ("cilindrada (displacement)", "eficiencia del combustible", "potencia",
# "peso" o "cilindros")
no_continua <- "elegir"
corregir(list("1. potencia", potencia, "color"),
list("2. peso", peso, "tamaño"),
list("3. cilindros", cilindros, "forma"),
list("4. la única no continua", no_continua, "cilindros"))⚪ 1. potencia — todavía sin responder
⚪ 2. peso — todavía sin responder
⚪ 3. cilindros — todavía sin responder
⚪ 4. la única no continua — todavía sin responder
Dos cosas para registrar antes de arrancar con lo nuestro.
La primera: una variable más no pide un gráfico nuevo, pide un atributo más. Arriba tenemos el mismo diagrama de dispersión de recién, con tres canales extra encendidos. Y fijate cuál se llevó la forma: los cilindros, la única variable discreta de las cinco. No es casualidad, es la partición del punto 3 de acá arriba: la forma no sabe representar valores continuos.
La segunda: los canales no rinden todos igual. La cilindrada y la eficiencia se leen sin esfuerzo; la potencia, más o menos; el peso y los cilindros, apenas. Cada canal que se enciende cuesta legibilidad. Sobre el final de la clase vamos a ver qué pasa cuando se encienden todos juntos.
Con esto ya tenemos la idea de la clase. Ahora, el trabajo.
Hoy, ustedes son el equipo de ciencia de datos de Nimbus. Recursos Humanos quiere saber si conviene extender el piloto de fruta a todas las sedes el año que viene. Esta notebook, cuando la terminemos, va a ser el informe que le mandemos.
| # | Paso | Qué hacemos |
|---|---|---|
| 0 | Preparación | cargar los datos |
| 1 | El criterio | auditar los gráficos que ya mandó RRHH |
| 2 | El mapeo | cómo se escribe un mapeo en ggplot2 |
| 3 | El repertorio | qué gráfico contesta cada pregunta |
| 4 | Las decisiones | dos gráficos correctos, ¿cuál mandamos? |
| 5 | Componer y comunicar | varias figuras que hablen el mismo idioma |
| 6 | El informe | la figura final y la conclusión |
Casi todos los pasos terminan con una consigna corta o con un quiz que se corrige solo. Están pensadas para resolverse en cinco o diez minutos durante la clase.
0. Preparación
Hoy no hay que subir nada a Drive: los datos se leen directo por URL desde el sitio de la materia. Son los tres archivos de Nimbus que ya conocés de la Clase 2.
La celda de abajo también define la paleta de colores (i.e. los colores que usamos en la cátedra) y crea una carpeta figuras/: cada figura del informe la vamos a guardar ahí como archivo .png con ggsave(...). Eso es parte del tema de hoy: las figuras de un informe no viven en la pantalla de colab, viven en archivos que se pueden mandar y regenerar.
library(readr) # leer archivos
library(dplyr) # manipular tablas
library(tidyr) # reacomodar tablas
library(ggplot2) # graficar
library(patchwork) # poner varias figuras en una misma hoja
# Los datos se leen por URL: no hay que bajar ni montar nada.
# (read_csv reconoce solo que "fecha" es una fecha, no un texto.)
BASE <- "https://analiticadedatos-udesa.com/data/toy-nimbus/"
empleados <- read_csv(paste0(BASE, "nimbus_empleados.csv"), show_col_types = FALSE)
salario <- read_csv(paste0(BASE, "nimbus_salario.csv"), show_col_types = FALSE)
bienestar <- read_csv(paste0(BASE, "nimbus_bienestar_diario.csv"), show_col_types = FALSE)
# Una carpeta para ir guardando las figuras del informe.
dir.create("figuras", showWarnings = FALSE)
cat("empleados:", dim(empleados), "| salario:", dim(salario),
"| bienestar:", dim(bienestar), "\n")empleados: 600 7 | salario: 1800 4 | bienestar: 24000 5
#@title Estilo de las figuras del informe (ejecutar, no hace falta leer) {display-mode: "form"}
# Colores y estilo de la catedra: hace que todas las figuras del informe hablen el mismo
# idioma visual. Por que eso importa se ve en la seccion 5.
library(ggplot2)
PALETA <- c("#00529B", "#C0492F", "#7E9EBB", "#1F7A4D", "#B4232E", "#9FB0BD")
theme_set(theme_minimal(base_size = 12))
options(ggplot2.discrete.colour = PALETA, ggplot2.discrete.fill = PALETA)Como repaso de la clase pasada, ¿qué es una fila en cada tabla?
Los tres archivos hablan de la misma gente, pero cada uno tiene su propia unidad de observación: lo que representa una fila cambia de tabla en tabla. Esa es la pregunta que hay que contestar antes de hacer cualquier inner_join.
| tabla | una fila es… | filas |
|---|---|---|
empleados |
un empleado | 600 |
salario |
un empleado en un año | 1.800 = 600 × 3 años |
bienestar |
un empleado en un día | 24.000 = 600 × 40 días |
inner_join pega las columnas de una tabla en otra usando una columna en común, acá empleado_id. Lo importante es anticipar qué le pasa a las filas:
empleados (600 filas) bienestar (24.000 filas)
1 fila = 1 empleado 1 fila = 1 empleado en 1 día
empleado_id sede area empleado_id fecha bienestar
1 Buenos Aires Ventas 1 2025-03-03 4
2 Mar del Plata Ingenieria 1 2025-03-04 5
3 Mendoza Producto 1 2025-03-05 3
... 1 ... ...
2 2025-03-03 6
│ │
└────────── by = "empleado_id" ─────┘
El empleado 1 aparece 1 vez a la izquierda y 40 veces a la derecha:
"Buenos Aires" y "Ventas" van a quedar REPETIDOS en sus 40 filas diarias.
El resultado no tiene 600 filas ni 24.600: sigue teniendo 24.000. El join no agrega filas, agrega columnas. Y eso es justo lo que necesitamos hoy: que cada medición diaria sepa de qué sede y de qué área viene, para después poder pedirle a ggplot2 (el paquete de R para visualizar datos) “separame por sede” o “coloreame por área”.
# Miremos las tres tablas antes de pegarlas.
cat("empleados:", dim(empleados), "\n"); print(head(empleados, 3))
cat("\nsalario:", dim(salario), "\n"); print(head(salario, 3))
cat("\nbienestar:", dim(bienestar), "\n"); print(head(bienestar, 3))
# La pregunta que decide como sale el join: ¿cuantas veces aparece UN empleado?
cat("\nel empleado 1 aparece...\n")
cat(" en empleados:", sum(empleados$empleado_id == 1), "vez\n")
cat(" en salario: ", sum(salario$empleado_id == 1), "veces (una por año)\n")
cat(" en bienestar:", sum(bienestar$empleado_id == 1), "veces (una por dia)\n")empleados: 600 7
# A tibble: 3 × 7
empleado_id sede area genero educacion_nivel antiguedad_anios grupo_fruta
<dbl> <chr> <chr> <chr> <chr> <dbl> <chr>
1 1 Buenos … Vent… Mascu… Universitario 8 Tratamiento
2 2 Mar del… Inge… Femen… Posgrado 8 Control
3 3 Mendoza Prod… Femen… Universitario 4 Tratamiento
salario: 1800 4
# A tibble: 3 × 4
empleado_id anio edad salario_mensual
<dbl> <dbl> <dbl> <dbl>
1 1 2023 45 1066000
2 2 2023 44 1201000
3 3 2023 29 1025000
bienestar: 24000 5
# A tibble: 3 × 5
empleado_id fecha fase grupo_fruta bienestar
<dbl> <date> <chr> <chr> <dbl>
1 1 2025-03-03 baseline Tratamiento 4
2 1 2025-03-04 baseline Tratamiento 5
3 1 2025-03-05 baseline Tratamiento 3
el empleado 1 aparece...
en empleados: 1 vez
en salario: 3 veces (una por año)
en bienestar: 40 veces (una por dia)
# JOIN 1: a cada empleado le pegamos su salario.
# Ojo: salario tiene 3 filas por empleado (2023, 2024, 2025). Si las pegaramos
# todas, cada empleado aparecería 3 veces en el resultado. Como para el informe
# alcanza con el sueldo actual, primero nos quedamos con un solo año.
salario_2025 <- salario |> filter(anio == 2025)
cat("salario:", dim(salario), " -> salario_2025:", dim(salario_2025), "\n")
# Ahora si: 1 fila por empleado de cada lado -> 1 fila por empleado en el resultado.
emp <- empleados |> inner_join(salario_2025, by = "empleado_id")
cat("emp:", dim(emp), " (las mismas 600 personas, con 3 columnas nuevas)\n")
head(emp, 3)salario: 1800 4 -> salario_2025: 600 4
emp: 600 10 (las mismas 600 personas, con 3 columnas nuevas)
| empleado_id | sede | area | genero | educacion_nivel | antiguedad_anios | grupo_fruta | anio | edad | salario_mensual |
|---|---|---|---|---|---|---|---|---|---|
| <dbl> | <chr> | <chr> | <chr> | <chr> | <dbl> | <chr> | <dbl> | <dbl> | <dbl> |
| 1 | Buenos Aires | Ventas | Masculino | Universitario | 8 | Tratamiento | 2025 | 47 | 1301000 |
| 2 | Mar del Plata | Ingenieria | Femenino | Posgrado | 8 | Control | 2025 | 46 | 1329000 |
| 3 | Mendoza | Producto | Femenino | Universitario | 4 | Tratamiento | 2025 | 31 | 1255000 |
El segundo join es de otro tipo, y es el que conviene mirar con atención.
Acá el de la izquierda es bienestar, que tiene 40 filas por empleado. Cada empleado aparece una sola vez en empleados, así que su sede y su área se van a copiar en las 40 filas que le corresponden. La tabla no se agranda a lo largo (sigue teniendo 24.000 filas), se agranda a lo ancho.
# JOIN 2: a cada medicion diaria le pegamos la sede y el area de quien la reporto.
# De empleados nos llevamos SOLO las columnas que vamos a usar, mas la clave.
# (Si pegaramos la tabla entera, arrastrariamos genero, educacion, etc. sin necesidad.)
datos_del_empleado <- empleados |> select(empleado_id, sede, area)
bien <- bienestar |> inner_join(datos_del_empleado, by = "empleado_id")
cat("bien:", dim(bien), " (las mismas 24.000 mediciones, con 2 columnas nuevas)\n")
# La prueba de lo que dijimos arriba: las primeras filas del empleado 1.
# "Buenos Aires" y "Ventas" aparecen repetidos, uno por cada dia que reporto.
bien |> filter(empleado_id == 1) |> head(4)bien: 24000 7 (las mismas 24.000 mediciones, con 2 columnas nuevas)
| empleado_id | fecha | fase | grupo_fruta | bienestar | sede | area |
|---|---|---|---|---|---|---|
| <dbl> | <date> | <chr> | <chr> | <dbl> | <chr> | <chr> |
| 1 | 2025-03-03 | baseline | Tratamiento | 4 | Buenos Aires | Ventas |
| 1 | 2025-03-04 | baseline | Tratamiento | 5 | Buenos Aires | Ventas |
| 1 | 2025-03-05 | baseline | Tratamiento | 3 | Buenos Aires | Ventas |
| 1 | 2025-03-06 | baseline | Tratamiento | 5 | Buenos Aires | Ventas |
Tres preparativos más y ya podemos graficar.
El primero es una columna que no existía: la fecha nos dice el día exacto, pero para el informe vamos a querer hablar de semanas del piloto. La construimos.
El segundo es una decisión de informe, y conviene tomarla ahora y no cuando aparezca el primer gráfico: fijar el orden de los grupos (Tratamiento y Control). Si no lo hacemos, cada gráfico los ordena como quier (i.e. primero Tratamiento y despues Control, o al reves), y el informe termina estando desordenado (algo que vamos a criticar más adelante en los gráficos de otros).
El tercero es una unidad. Los salarios de Nimbus tienen siete dígitos, y un eje lleno de números como 1250000 no lo lee nadie. Nos guardamos el salario también en millones de pesos, y de ahí en adelante graficamos esa columna, diciendo la unidad en el título del eje.
# La fecha dice el dia exacto (2025-03-03). Para el informe queremos algo mas simple:
# en que SEMANA del piloto estamos. Se construye en dos pasos.
primer_dia <- min(bien$fecha)
cat("el piloto arranco el", format(primer_dia), "\n\n")
# PASO 1: cuantos dias pasaron desde ese primer dia.
# Restar dos fechas no da un numero, da una duracion (R la muestra como
# "7 days"). as.integer() se queda con el numero pelado.
# PASO 2: cada bloque de 7 dias es una semana.
# %/% es la division ENTERA: divide y tira los decimales.
# dias 0 a 6 -> 0 dias 7 a 13 -> 1 dias 14 a 20 -> 2
# El +1 es para que la primera semana se llame 1 y no 0.
bien <- bien |>
mutate(dias_desde_el_inicio = as.integer(fecha - primer_dia),
semana = dias_desde_el_inicio %/% 7 + 1)
# Miremoslo funcionando, en los primeros dias de un empleado:
bien |>
filter(empleado_id == 1) |>
select(fecha, dias_desde_el_inicio, semana) |>
head(11) |>
as.data.frame() |>
print(row.names = FALSE)el piloto arranco el 2025-03-03
fecha dias_desde_el_inicio semana
2025-03-03 0 1
2025-03-04 1 1
2025-03-05 2 1
2025-03-06 3 1
2025-03-07 4 1
2025-03-10 7 2
2025-03-11 8 2
2025-03-12 9 2
2025-03-13 10 2
2025-03-14 11 2
2025-03-17 14 3
Mirá el salto del día 4 al día 7: los días 5 y 6 son sábado y domingo, y el piloto sólo tiene días hábiles, así que no existen en los datos. La cuenta funciona igual porque contamos días de calendario desde el arranque, no filas de la tabla. Si hubiéramos contado filas (“cada 5 mediciones, una semana”), el fin de semana la habría roto.
Ahora sí, con la semana ya construida, podemos ver la forma del piloto:
# Cada fase cubre 4 semanas: primero todos sin fruta, despues solo un grupo con fruta.
tapply(bien$semana, bien$fase, function(s) sort(unique(s)))- $baseline
-
- 1
- 2
- 3
- 4
- $intervencion
-
- 5
- 6
- 7
- 8
# 2) El orden de los grupos, fijado de una vez para toda la clase:
# Control primero (navy), Tratamiento segundo (terracota).
# factor() le pega ese orden a la columna, y ggplot2 lo respeta al asignar
# colores y al ubicar las categorias en los ejes.
ORDEN_GRUPO <- c("Control", "Tratamiento")
bien <- bien |> mutate(grupo_fruta = factor(grupo_fruta, levels = ORDEN_GRUPO))
emp <- emp |> mutate(grupo_fruta = factor(grupo_fruta, levels = ORDEN_GRUPO))
cat("orden fijado:", levels(bien$grupo_fruta), "\n\n")
# 3) El salario en millones, para que los ejes se lean.
emp <- emp |> mutate(salario_millones = salario_mensual / 1e6)
emp |> select(salario_mensual, salario_millones) |> head(3) |>
as.data.frame() |> print(row.names = FALSE)orden fijado: Control Tratamiento
salario_mensual salario_millones
1301000 1.301
1329000 1.329
1255000 1.255
Y con esto ya tenemos el número que RRHH quiere entender.
# group_by con DOS columnas: devuelve una fila por cada combinacion de fase y grupo.
resumen_fases <- bien |>
group_by(fase, grupo_fruta) |>
summarise(bienestar = round(mean(bienestar), 2))
print(resumen_fases)
# Los cuatro numeros que el texto de abajo cita salen de aca, no estan escritos a
# mano: si alguno cambiara, la notebook se corta en esta linea.
stopifnot(all(abs(sort(resumen_fases$bienestar) - c(4.60, 4.61, 4.62, 4.95)) < 0.011))`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by fase and grupo_fruta.
ℹ Output is grouped by fase.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(fase, grupo_fruta))` for per-operation grouping
(`?dplyr::dplyr_by`) instead.
# A tibble: 4 × 3
# Groups: fase [2]
fase grupo_fruta bienestar
<chr> <fct> <dbl>
1 baseline Control 4.62
2 baseline Tratamiento 4.6
3 intervencion Control 4.61
4 intervencion Tratamiento 4.95
(Paréntesis: el mensaje que sale arriba, summarise() has regrouped the output ... Output is grouped by fase, no es un error. Es un aviso del paquete dplyr de que la tabla que devolvió quedó todavía agrupada por la primera columna, así que si seguís operando sobre ella las cuentas se van a hacer por grupo sin que vos lo pidas. Se apaga agregando .groups = "drop" al summarise(), que es lo que vamos a hacer de acá en adelante. Los avisos de este tipo no rompen nada: existen para que uno no arrastre sin darse cuenta un estado que después le cambia los números.)
Ahí está el panorama. En la fase baseline, antes de que hubiera fruta en ningún lado, los dos grupos están prácticamente iguales: 4,62 y 4,60. En la fase de intervención, el grupo con fruta marca 4,95 y el de control se queda en 4,61.
Ahora, cuidado con lo que podemos afirmar. Son cuatro promedios. Un promedio solo no distingue una diferencia real del ruido: no dice nada sobre cuánto varían las respuestas, ni sobre cuánta gente hay detrás de cada número. Decidir si una diferencia como esta es atribuible al programa, y no al azar, es una pregunta de inferencia, con las herramientas que traen de Estadística y que acordamos como base en la Clase 1.
Y esa no es la pregunta de hoy. La pregunta de hoy es cómo se muestra esto. Un gráfico honesto de estos datos tiene que dejar ver no sólo el tercio de punto de diferencia, sino también cuánto se solapan los grupos, para que quien lo lea pueda juzgar por su cuenta. Ahí está lo interesante del encargo: con estos mismos cuatro números vamos a poder construir una figura que sugiera un efecto contundente y otra que sugiera que no pasó nada, sin cambiar un solo dato.
1. El criterio
Para analizar como se componen los gráficos, necesitamos un vocabulario, y lo tomamos del libro de la clase (Wilke, Fundamentals of Data Visualization, cap. 1). Una figura puede fallar de tres maneras distintas:
| veredicto | el problema es de… | en palabras de Wilke |
|---|---|---|
| fea | estética | tiene problemas estéticos, pero por lo demás es clara e informativa |
| mala | percepción | es poco clara, confusa, innecesariamente complicada o engañosa |
| incorrecta | matemática | es objetivamente incorrecta |
Tres precisiones antes de usarlo:
- No hay categoría “buena”: toda figura sin veredicto es, al menos, aceptable.
- Los bordes son difusos, sobre todo entre fea y mala, y Wilke lo admite. Se puede discutir.
- Es una escala de gravedad. Pregunta para pensar: ¿qué preferirías que se mande, una figura fea o una mala?
Veámoslo primero con el ejemplo del propio libro recreado acá con tres números inventados (A=3, B=5, C=4). Cuatro versiones del mismo dato, una por celda.
Las cuatro figuras de abajo son las del libro, recreadas. NO hace falta que entiendas el código: usan detalles de ggplot2 que no vamos a enseñar hoy, y acá lo que importa es mirarlas, no fabricarlas. Corré cada celda y mirá el resultado.
#@title Figura 1.1 del libro, version (a): aceptable {display-mode: "form"}
# Los datos del ejemplo del libro: tres categorias, tres valores.
df_abc <- data.frame(tipo = c("A", "B", "C"), valor = c(3, 5, 4))
# Version (a), la aceptable: un color, un eje que arranca en cero, nada de sobra.
p <- ggplot(df_abc, aes(x = tipo, y = valor)) +
geom_col(fill = "#00529B", width = 0.7) +
labs(x = NULL, y = "valor", title = "(a) aceptable")
# ggsave guarda la figura como archivo png; dpi es la resolucion, y width/height
# el tamano de la hoja en pulgadas.
options(repr.plot.width = 5, repr.plot.height = 3)
ggsave("figuras/clase03_triada_a_r.png", p, dpi = 150, width = 5, height = 3, bg = "white")
p
#@title Figura 1.1, version (b): FEA {display-mode: "form"}
# Version (b), la FEA: colores chillones que no codifican nada, grilla que grita,
# tipografias mezcladas. Los numeros se leen igual... pero cuesta mirarla.
p <- ggplot(df_abc, aes(x = tipo, y = valor, fill = tipo)) +
geom_col(width = 0.7) +
scale_fill_manual(values = c("#aaff00", "#00ffff", "#cc00cc"), guide = "none") +
labs(x = NULL, y = "valor", title = "(b) FEA: correcta, pero cuesta mirarla") +
theme(panel.grid = element_line(colour = "black", linewidth = 0.7),
axis.title.y = element_text(size = 15, family = "serif"),
plot.title = element_text(family = "mono"))
options(repr.plot.width = 5, repr.plot.height = 3)
ggsave("figuras/clase03_triada_b_r.png", p, dpi = 150, width = 5, height = 3, bg = "white")
p
#@title Figura 1.1, version (c): MALA {display-mode: "form"}
# Version (c), la MALA: cada barra en su propio panel, y cada panel con SU eje.
# Ningun numero es falso, y sin embargo las tres barras "quedan iguales".
panel <- function(tipo, valor, tope) {
ggplot(data.frame(tipo = tipo, valor = valor), aes(x = tipo, y = valor)) +
geom_col(fill = "#00529B", width = 0.55) +
coord_cartesian(ylim = c(0, tope)) + # el tope del eje, panel por panel
labs(x = NULL, y = "valor")
}
# El | de patchwork pone dos figuras lado a lado.
p <- (panel("A", 3, 3.2) | panel("B", 5, 5.2) | panel("C", 4, 4.3)) +
plot_annotation(title = "(c) MALA: tres ejes distintos, las barras 'quedan iguales'")
options(repr.plot.width = 8, repr.plot.height = 3)
ggsave("figuras/clase03_triada_c_r.png", p, dpi = 150, width = 8, height = 3, bg = "white")
p
#@title Figura 1.1, version (d): INCORRECTA {display-mode: "form"}
# Version (d), la INCORRECTA: sin escala en el eje y, los numeros no se pueden
# recuperar. La figura dejo de ser una afirmacion verificable.
p <- ggplot(df_abc, aes(x = tipo, y = valor)) +
geom_col(fill = "#00529B", width = 0.7) +
labs(x = NULL, y = NULL, title = "(d) INCORRECTA: no hay forma de leer los valores") +
theme(axis.text.y = element_blank(), # borrar las etiquetas del eje y
panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank())
options(repr.plot.width = 5, repr.plot.height = 3)
ggsave("figuras/clase03_triada_d_r.png", p, dpi = 150, width = 5, height = 3, bg = "white")
p
La versión (c) es la más peligrosa de las cuatro: ningún número está mal, ningún eje está truncado, cada panel leído por separado es honesto. Y sin embargo la comparación visual miente, porque B ya no se ve mayor que A. Guardá esa idea: vuelve al final de la clase.
Ahora sí, los dos gráficos que mandó RRHH, hechos con los datos reales de Nimbus.
Un minuto de ggplot2, y no volvemos
Todos los gráficos de hoy se escriben igual, porque ggplot2 arma el gráfico por capas y las capas se suman con un +. Tres capas alcanzan para casi todo:
ggplot(empleados, aes(y = sede)) + # 1. la tabla y el MAPEO
geom_bar(fill = "#00529B") + # 2. la geometria: que se dibuja
labs(title = "Empleados por sede", # 3. las palabras
x = "empleados", y = NULL)
Dos cosas para registrar:
aes()es el mapeo, y nada más que el mapeo. Adentro van las columnas que se conectan con un atributo gráfico (x,y,colour,fill,shape,size). Lo que no depende de los datos, como pintar todas las barras del mismo azul, va afuera deaes(). Esa frontera es exactamente la distinción del capítulo 2 de Wilke, escrita en código.- Un gráfico de ggplot2 es un objeto, no un dibujo que ya se fue a la pantalla. Se puede guardar en una variable (
p <- ggplot(...) + ...), mostrarlo escribiendop, guardarlo a archivo conggsave("figuras/sedes.png", p, dpi = 150), o seguir sumándole capas después.
Varios paneles en una hoja. Como cada gráfico es un objeto, poner dos al lado del otro es literalmente sumarlos, con el paquete patchwork:
p1 | p2 # lado a lado
p1 / p2 # uno encima del otro
(p1 | p2) + plot_annotation(title = "el titulo de la hoja entera")
La diferencia entre labs(title = ...) y plot_annotation(title = ...) es la misma que entre el título de un panel y el título de la hoja: cuando hay varios paneles, cada uno se titula por su cuenta y la hoja se titula aparte.
Eso es todo lo que hace falta de ggplot2 para hoy.
Ahora si, volvemos a Nimbus
RRHH no nos esperó: ya armó sus propios gráficos en una planilla y los adjuntó al pedido. Antes de producir nada, nuestro primer trabajo es auditar lo que mandaron.
Antes de mirarlos, un minuto de sintaxis, porque de acá en adelante todos los gráficos de la clase se escriben igual. Una llamada típica se lee así:
ggplot( empleados , aes(y = area) ) + geom_bar( fill = "#00529B" )
↑ ↑ ↑
de qué tabla qué columna va a de qué color
salen los qué atributo gráfico lo pinto
datos (el MAPEO)
Tres cosas que conviene registrar desde ahora:
ggplot(...)abre la hoja y recibe la tabla entera. Nunca se le pasan los datos sueltos: siempre una tabla y, adentro deaes(), nombres de columnas de esa tabla (acá sí, sin comillas).geom_adelante: la geometría dice qué tipo de gráfico es.geom_barcuenta filas por categoría,geom_boxplothace cajas,geom_pointhace nubes de puntos.- Lo que va adentro de
aes()depende de los datos; lo que va afuera es una decisión fija nuestra.
El por qué la librería está diseñada así ya lo vimos al abrir la clase, y en la sección 2 lo retomamos, ahora escrito en código. Por ahora alcanza con poder leer las líneas.
#@title El grafico que mando RRHH {display-mode: "form"}
# Ilustracion: R no tiene barras 3D, asi que las dibujamos proyectando cajas a mano.
# NO hace falta leer nada de esta celda: lo unico que importa es la figura que sale.
conteo_sedes <- empleados |> count(sede, sort = TRUE)
n_sedes <- nrow(conteo_sedes)
alto_max <- max(conteo_sedes$n)
# La camara: azimut -60 grados, elevacion 22, a distancia 9 del centro de la escena.
azim <- -60 * pi / 180; elev <- 22 * pi / 180; dist <- 9
cam <- dist * c(cos(elev) * cos(azim), cos(elev) * sin(azim), sin(elev))
hacia <- -c(cos(elev) * cos(azim), cos(elev) * sin(azim), sin(elev))
der <- c(-sin(azim), cos(azim), 0)
arr <- c(-cos(azim) * sin(elev), -sin(azim) * sin(elev), cos(elev))
# Proyeccion en PERSPECTIVA: se divide por la distancia a la camara, y por eso lo
# que esta mas cerca se dibuja mas grande. Ese es todo el truco del 3D.
proyectar <- function(x, y, z) {
p <- cbind((x - n_sedes / 2) / (n_sedes / 2), (y - 0.5) * 0.7, z / alto_max * 1.6 - 0.8)
v <- sweep(p, 2, cam)
prof <- as.vector(v %*% hacia)
data.frame(sx = as.vector(v %*% der) / prof, sy = as.vector(v %*% arr) / prof)
}
# Los seis colores del arcoiris, los mismos que sale por defecto en una planilla.
arcoiris <- c("#8000ff", "#1996f3", "#4df3ce", "#b2f396", "#ff964f", "#ff0000")
oscurecer <- function(hex, k) rgb(t(col2rgb(hex) * k), maxColorValue = 255)
# Cada barra son tres caras: el frente, el lado derecho y el techo. Se dibujan de
# la mas lejana a la mas cercana, para que la de adelante tape a la de atras.
cara <- function(i, xs, ys, zs, que, color)
cbind(proyectar(xs, ys, zs), id = paste0(i, que), col = color)
caras <- do.call(rbind, lapply(seq_len(n_sedes), function(i) {
x0 <- i - 1; x1 <- x0 + 0.62; h <- conteo_sedes$n[i]; y0 <- 0.08; y1 <- 0.92
rbind(cara(i, c(x1, x1, x1, x1), c(y0, y1, y1, y0), c(0, 0, h, h), "lado",
oscurecer(arcoiris[i], 0.55)),
cara(i, c(x0, x1, x1, x0), c(y0, y0, y1, y1), c(h, h, h, h), "techo",
oscurecer(arcoiris[i], 0.80)),
cara(i, c(x0, x1, x1, x0), rep(y0, 4), c(0, 0, h, h), "frente", arcoiris[i]))
}))
# Los nombres de las sedes, abajo de cada barra y rotados 90 grados.
etiquetas <- do.call(rbind, lapply(seq_len(n_sedes), function(i) {
pt <- proyectar(i - 1 + 0.31, 0.08, 0)
data.frame(sx = pt$sx, sy = pt$sy - 0.004, sede = conteo_sedes$sede[i])
}))
# El eje de la altura, flotando a la derecha (asi sale de la planilla).
marcas <- seq(0, 100, by = 20)
eje <- proyectar(rep(n_sedes + 0.15, length(marcas)), rep(0.92, length(marcas)), marcas)
eje$lab <- marcas
rotulo <- proyectar(n_sedes + 0.15, 0.92, 60)
p <- ggplot(caras, aes(x = sx, y = sy, group = id, fill = col)) +
geom_polygon(colour = "white", linewidth = 0.35) +
scale_fill_identity() +
geom_text(data = etiquetas, aes(x = sx, y = sy, label = sede),
angle = 90, hjust = 1, size = 2.6, colour = "#333333", inherit.aes = FALSE) +
geom_point(data = eje, aes(x = sx, y = sy), shape = 45, size = 3,
colour = "#333333", inherit.aes = FALSE) +
geom_text(data = eje, aes(x = sx, y = sy, label = lab), hjust = -0.8,
size = 2.9, colour = "#333333", inherit.aes = FALSE) +
geom_text(data = rotulo, aes(x = sx, y = sy, label = "cant"), hjust = -1.9,
size = 3.1, colour = "#333333", inherit.aes = FALSE) +
labs(title = "Empleados por sede") +
coord_fixed(clip = "off") +
theme_void(base_size = 12) +
theme(plot.title = element_text(family = "serif", size = 15, hjust = 0.5),
plot.margin = margin(10, 40, 30, 10))
options(repr.plot.width = 8.5, repr.plot.height = 5)
ggsave("figuras/clase03_auditoria_rrhh1_r.png", p, dpi = 150, width = 8.5, height = 5, bg = "white")
p
Eso es lo que RRHH mandó adjunto al pedido: cuántos empleados tiene cada sede.
Antes de seguir, contestá dos preguntas mirando sólo esta figura.
- ¿Qué sede tiene más empleados? Anotá tu respuesta, la vamos a usar en un minuto.
- ¿Qué le criticarías a este gráfico? Hacé la lista de todo lo que te moleste, sin filtrar: los colores, las letras, las líneas, lo que sea.
Ahora hagamos nosotros el mismo gráfico, con los mismos datos y sin ningún adorno.
# El mismo dato, en un grafico plano y de un solo color.
orden_sedes <- conteo_sedes$sede # las sedes, de mayor a menor
# Ojo con el eje y: ggplot2 pone el PRIMER nivel del factor abajo de todo, asi que
# la lista se da al reves para que la sede mas grande quede arriba.
p <- ggplot(empleados, aes(y = factor(sede, levels = rev(orden_sedes)))) +
geom_bar(fill = "#00529B") +
labs(x = "empleados", y = NULL, title = "Empleados por sede")
options(repr.plot.width = 7, repr.plot.height = 3.4)
ggsave("figuras/clase03_auditoria_rrhh1_ok_r.png", p, dpi = 150, width = 7, height = 3.4, bg = "white")
p
# Los dos numeros que el texto de abajo cita, calculados y verificados.
stopifnot(conteo_sedes$sede[1] == "Buenos Aires", conteo_sedes$n[1] == 109,
conteo_sedes$sede[n_sedes] == "Mendoza", conteo_sedes$n[n_sedes] == 93)
Empecemos por la pregunta que te pedí que te guardaras. Si mirando el gráfico de RRHH dijiste Mendoza, es la respuesta que la figura invita a dar: es la barra de adelante y se ve enorme. Pero Mendoza es la más chica de las seis (93 empleados). La más grande es Buenos Aires (109), que allá atrás se ve modesta. La perspectiva sola podría dar vuelta el orden real.
Ahora sí, la lista completa. Son cuatro defectos, y no son todos de la misma gravedad:
| defecto | veredicto | por qué |
|---|---|---|
| Un color por barra | feo | La sede ya está escrita abajo de cada barra: el color repite lo que el eje dice, y a cambio mete seis tonos que compiten entre sí |
| La grilla marcada | feo | Las líneas pesan casi tanto como los datos, cuando su único trabajo es ayudar a estimar de reojo |
| Los nombres verticales | feo | Hay que inclinar la cabeza para leer cada uno. Es lo primero que se le ocurre a cualquiera cuando las etiquetas no entran, y la salida no es rotarlas: es dar vuelta el gráfico |
| El 3D | malo | Cambia lo que percibís, no sólo lo que te cuesta leer |
Los tres primeros son molestos, pero no te impiden llegar al número correcto: te hacen trabajar de más. Eso es feo.
El 3D es otra cosa, y por eso el veredicto cambia. La profundidad ahí no codifica nada: las seis sedes están apoyadas en la misma línea, y la tercera dimensión es pura decoración. Pero al proyectar un objeto 3D sobre una pantalla plana, lo que está más cerca se agranda — así que la decoración cambió lo que viste. Sumale que el eje de la altura quedó flotando en el medio, sin alinearse con ninguna barra, y que dice “cant” en vez de decir qué se está contando.
Fijate que el gráfico de abajo no es más elaborado que el de arriba: es menos. Le sacamos cosas, y por eso se lee.
Una última cosa sobre el 3D, porque es el caso más discutido de los cuatro: la intuición dice feo, y Wilke lo etiqueta malo, pero con un argumento matemático (incorrecto). Es el mejor ejemplo de que el borde entre malo e incorrecto es poroso. Las excepciones para 3D que el libro sí admite son las visualizaciones interactivas que se pueden rotar, u objetos que son tridimensionales de verdad.
#@title El segundo grafico que mando RRHH {display-mode: "form"}
# Salario promedio por sede, "para mostrar las brechas". Tal como llego.
medias_rrhh <- emp |>
group_by(sede) |>
summarise(salario_mensual = mean(salario_mensual), .groups = "drop")
orden_rrhh <- medias_rrhh |> arrange(desc(salario_mensual)) |> pull(sede)
p <- ggplot(medias_rrhh, aes(x = salario_mensual, y = factor(sede, levels = rev(orden_rrhh)))) +
geom_col(fill = "#00529B") +
coord_cartesian(xlim = c(1243000, 1272000)) + # el eje NO arranca en cero
labs(x = "salario_mensual", y = NULL, title = "La brecha salarial entre sedes")
options(repr.plot.width = 7, repr.plot.height = 3.4)
ggsave("figuras/clase03_auditoria_rrhh2_r.png", p, dpi = 150, width = 7, height = 3.4, bg = "white")
p
Ese es el segundo adjunto. RRHH lo mandó para mostrar las brechas entre sedes, y la conclusión que sacaron es que Córdoba paga muchísimo más que Mar del Plata.
Otra vez, dos preguntas mirando sólo la figura.
- ¿Cuánto más paga Córdoba que Mar del Plata? Contestá con un número: ¿un 10% más? ¿el doble? ¿diez veces más?
- ¿En qué número arranca el eje de abajo? Miralo bien antes de seguir.
Ahora el mismo dato, en un gráfico igual en todo salvo en una cosa.
# El mismo dato y el mismo tipo de grafico, con dos arreglos: el eje arranca en cero
# (una barra lo hace sola, si uno no le pide otra cosa) y el salario va en millones,
# con la unidad dicha en el titulo del eje.
medias_sede <- emp |>
group_by(sede) |>
summarise(salario_millones = mean(salario_millones), .groups = "drop")
orden_sedes_salario <- medias_sede |> arrange(desc(salario_millones)) |> pull(sede)
p <- ggplot(medias_sede,
aes(x = salario_millones, y = factor(sede, levels = rev(orden_sedes_salario)))) +
geom_col(fill = "#00529B") +
labs(x = "salario mensual promedio (millones de $)", y = NULL,
title = "Salario mensual promedio por sede")
options(repr.plot.width = 7, repr.plot.height = 3.4)
ggsave("figuras/clase03_auditoria_rrhh2_ok_r.png", p, dpi = 150, width = 7, height = 3.4, bg = "white")
p
# La brecha real entre la sede que mas paga y la que menos, que el texto cita.
alto <- max(medias_rrhh$salario_mensual); bajo <- min(medias_rrhh$salario_mensual)
cat(sprintf("Cordoba %.0f vs Mar del Plata %.0f -> %.1f%% de diferencia\n",
alto, bajo, 100 * (alto / bajo - 1)))
stopifnot(round(alto) == 1270296, round(bajo) == 1245407, 100 * (alto / bajo - 1) < 2.1)Cordoba 1270296 vs Mar del Plata 1245407 -> 2.0% de diferencia

Veredicto: incorrecta. Y acá no hay discusión de gusto posible.
Si contestaste “el doble” o “diez veces más”, la figura hizo exactamente lo que sus autores querían. La diferencia real entre Córdoba y Mar del Plata es del 2%: 1.270.296 pesos contra 1.245.407. Es lo que muestra el segundo gráfico, donde las seis barras se ven casi iguales, porque casi iguales es lo que son.
El problema es de aritmética. Una barra dice “cuánto” con su longitud: la de Córdoba tiene que ser un 2% más larga que la de Mar del Plata, porque el salario es un 2% mayor. Al arrancar el eje en 1.243.000 en vez de en cero, cada barra dejó de medir el salario y pasó a medir cuánto sobra por encima de 1.243.000, que no es un número que le interese a nadie. Ahí la barra de Córdoba quedó más de diez veces más larga que la de Mar del Plata.
Y hay dos arreglos más que hicimos de paso, los dos de la categoría fea. El eje de RRHH se titula salario_mensual, que es el nombre de la columna en la tabla y no dice qué se está midiendo ni en qué unidad. Y las marcas del eje son números de siete dígitos: para leer un salario hay que ponerse a contar ceros, y nadie en RRHH lo va a hacer. Nuestra versión pasa los salarios a millones y lo aclara en el título del eje.
Y ojo con la moraleja fácil: el problema no es cortar un eje, es cortarlo con barras. Cuando lleguemos al repertorio de gráficos vamos a ver la salida legítima para mostrar una diferencia chica sin exagerarla.
Con esto ya tenemos el criterio del día. Cada gráfico que aparezca de acá en adelante, propio o ajeno, se puede someter a la misma pregunta: ¿es feo, malo o incorrecto? (o ninguna de las tres, que es lo que vamos a intentar).
Como dice Wilke al cerrar su primer capítulo: “los animo a desarrollar su propio ojo y a evaluar críticamente mis decisiones”. Vale también para los gráficos de esta clase.
2. El mapeo, ahora en código
Al abrir la clase vimos la idea: un gráfico es un mapeo de variables a atributos gráficos, y elegir el mapeo es elegir qué se puede leer. Lo que sigue es cómo se escribe eso en ggplot2, sobre nuestros propios datos.
La gramática de ggplot2 es, literalmente, el mapeo
En ggplot2 el mapeo no está desparramado entre los argumentos: tiene una función propia, aes(), y ahí adentro se dice qué columna va a qué atributo. El nombre no es casualidad, sale del mismo lugar que el vocabulario de la clase: aesthetics.
| atributo (Wilke) | dentro de aes() (ggplot2) |
|---|---|
| posición | x =, y = |
| color | colour = (líneas y puntos), fill = (áreas y barras) |
| forma | shape = |
| tamaño | size = |
| ancho de línea | linewidth = |
| tipo de línea | linetype = |
Y con eso ya podemos hacer el movimiento central del bloque: el mismo dato, dos veces, con dos mapeos distintos. Es el bienestar promedio por sede a lo largo del piloto, con las mismas tres escalas (dos de posición y una de color) repartidas de dos maneras distintas.
# Version 1 del mapeo: la semana al eje x, el bienestar al eje y, la sede al color.
# Primero el promedio por sede y semana (una fila por punto de la linea).
serie_sede <- bien |>
group_by(sede, semana) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
p <- ggplot(serie_sede, aes(x = semana, y = bienestar, colour = sede)) +
geom_line(linewidth = 0.8) +
scale_x_continuous(breaks = 1:8) +
labs(x = "semana del piloto", y = "bienestar promedio (1-7)",
title = "Mapeo 1: el tiempo en x, el bienestar en y, la sede en color")
options(repr.plot.width = 8, repr.plot.height = 4)
ggsave("figuras/clase03_mapeo_lineas_r.png", p, dpi = 150, width = 8, height = 4, bg = "white")
p
Esa es una de las dos. Veamos la otra, que la escribimos juntos acá en clase.
El mismo dato, el otro mapeo
Queremos la versión permutada: los mismos números y las mismas tres escalas, pero con el bienestar en el color y la sede en el eje y. Como el color necesita superficie para poder leerse, cada combinación de sede y semana va a ser un cuadrado pintado.
Son dos decisiones, y conviene mirarlas en el código antes de correr la celda:
- El mapeo. Adentro de
aes()se decide qué columna va ax(la semana), cuál ay(la sede) y cuál afill, el color de relleno (el bienestar). - La geometría. La de ggplot2 que pinta un cuadrado por fila es
geom_tile(), por baldosa.
# Una fila por cada combinacion de sede y semana, con su bienestar promedio.
tabla <- bien |>
group_by(sede, semana) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
p <- ggplot(tabla, aes(x = semana, y = sede, fill = bienestar)) +
geom_tile() +
# geom_text escribe el numero adentro de cada cuadrado, con dos decimales.
geom_text(aes(label = sprintf("%.2f", bienestar)), colour = "white", size = 3) +
scale_fill_viridis_c(name = "bienestar promedio") +
scale_x_continuous(breaks = 1:8) +
labs(x = "semana del piloto", y = NULL,
title = "Mapeo 2: la sede en y, el bienestar en color")
options(repr.plot.width = 8, repr.plot.height = 4)
ggsave("figuras/clase03_mapeo_heatmap_r.png", p, dpi = 150, width = 8, height = 4, bg = "white")
p
Las dos figuras dicen los mismos números, y sin embargo no se leen igual. Probá contestar con cada una estas dos preguntas: ¿qué sede estuvo más abajo durante toda la semana 6? y ¿qué sede cruzó a cuál entre la semana 3 y la 4? Cada figura hace fácil una de las dos, y trabajosa la otra.
¿Alguna está mal? Ninguna, igual que con las temperaturas del principio de la clase.
Y hay una decisión que tomó ggplot2 por vos sin avisar: las sedes quedaron en orden alfabético (de abajo hacia arriba), porque es lo que hace por defecto con una columna de texto. Cualquier otro orden daría una figura distinta e igual de válida, y uno que dificulte la lectura sería feo, no un error.
3. El repertorio
Wilke organiza su catálogo de gráficos por el mensaje que comunican, no por el tipo de dato: cantidades, distribuciones, proporciones, relaciones x-y, datos geoespaciales, incertidumbre. La pregunta previa a todo gráfico del informe es entonces: ¿qué quiero que se comunique?
De las seis ramas hoy trabajamos dos a fondo, cantidades y distribuciones, que son las que más van a usar; relaciones y proporciones aparecen en el juego y en las consignas, y mapas e incertidumbre avanzada quedan sólo nombradas.
Un buen mapa interactivo de todo el repertorio de gráficos es: from Data to Viz (introduce temas por la forma de los datos; Wilke lo hace por el objetivo de comunicación).
Cantidades: barras, y las decisiones que las rodean
La orientación ya la vimos en la auditoría. Falta el orden de las barras, que es una decisión más consecuente de lo que parece.
La regla: Si las barras representan categorías sin orden propio, ordenalas por valor. El orden con el que vienen los datos (alfabético, o el de carga en la planilla) no significa nada, y dejarlo es una decisión que uno no tomó. Wilke marca ese caso como malo, no como feo: no es que quede desprolijo, es que la comparación entre barras se vuelve innecesariamente difícil.
Pero la regla tiene una excepción que casi siempre se olvida, y vale la pena verla:
orden_natural <- c("Secundario", "Terciario", "Universitario", "Posgrado")
orden_por_valor <- empleados |> count(educacion_nivel, sort = TRUE) |> pull(educacion_nivel)
p1 <- ggplot(empleados, aes(x = factor(educacion_nivel, levels = orden_por_valor))) +
geom_bar(fill = "#00529B") +
labs(x = NULL, y = "empleados", title = "Ordenado por valor: prolijo... y confuso")
p2 <- ggplot(empleados, aes(x = factor(educacion_nivel, levels = orden_natural))) +
geom_bar(fill = "#00529B") +
labs(x = NULL, y = "empleados", title = "Orden natural: la forma de la distribucion aparece")
options(repr.plot.width = 11, repr.plot.height = 3.4)
p1 | p2
La regla “ordená las barras por valor” tiene una excepción que casi siempre se olvida: vale sólo para categorías sin orden propio. Educación tiene un orden natural (Secundario, Terciario, Universitario, Posgrado), y respetarlo hace aparecer la forma de la distribución. Ordenarla por valor produce una figura más prolija y menos informativa: técnicamente correcta, pero mala, porque destruye la lectura.
Distribuciones
Un histograma no muestra “los datos”: muestra una interpretación gobernada por el ancho de bin (el ancho de la barra). Y si no lo elegiste vos, lo eligió el software.
Abajo está el mismo salario tres veces. El primer panel es lo que sale sin pedir nada: ¿quién eligió ese ancho? Un algoritmo, que no conoce ni los datos ni la pregunta. ggplot2 es honesto al respecto y lo avisa por escrito cada vez (stat_bin() using bins = 30. Pick better value), pero el aviso pasa desapercibido. Wilke lo dice sin vueltas: lo más probable es que ese ancho no sea el más apropiado para el histograma que uno quiere hacer. De ahí la regla, que es corta: probá varios anchos antes de quedarte con uno.
p1 <- ggplot(emp, aes(x = salario_millones)) +
geom_histogram(fill = "#00529B", colour = "white") +
labs(x = "salario mensual (millones de $)", y = "empleados",
title = "Sin pedir nada: el ancho lo eligio ggplot2")
p2 <- ggplot(emp, aes(x = salario_millones)) +
geom_histogram(binwidth = 0.01, fill = "#00529B", colour = "white") +
labs(x = "salario mensual (millones de $)", y = "empleados",
title = "binwidth = 0,01 (10 mil $): ruido")
p3 <- ggplot(emp, aes(x = salario_millones)) +
geom_histogram(binwidth = 0.40, fill = "#00529B", colour = "white") +
labs(x = "salario mensual (millones de $)", y = "empleados",
title = "binwidth = 0,40 (400 mil $): una sola barra")
options(repr.plot.width = 13, repr.plot.height = 3.4)
p1 | p2 | p3
# Proba: cambia algun binwidth y volve a correr la celda.`stat_bin()` using `bins = 30`. Pick better value `binwidth`.

Boxplot y violín: ver la distribución en poco espacio
Cuando lo que importa es el corrimiento entre grupos, la familia boxplot / violín resume cada distribución en una franja angosta. Son probablemente las dos visualizaciones más rendidoras de toda la clase, y el violín es el boxplot con la forma de la distribución dibujada encima.
Pero tienen un requisito. Un violín es una densidad, y una densidad es para variables continuas. Por eso acá van sobre el salario y no sobre el bienestar: entre dos salarios siempre existe uno intermedio, y entre el 4 y el 5 de una escala de 1 a 7 no existe nada.
El tercer panel muestra lo que corresponde cuando la variable es ordinal: una barra por cada valor que existe de verdad. Es la misma comparación entre grupos, sin inventar nada.
orden_educacion <- c("Secundario", "Terciario", "Universitario", "Posgrado")
# Barras horizontales: los nombres de los niveles no entran en el eje x.
# rev() otra vez, para que Secundario quede arriba y Posgrado abajo.
emp_edu <- emp |> mutate(nivel = factor(educacion_nivel, levels = rev(orden_educacion)))
p1 <- ggplot(emp_edu, aes(y = nivel, x = salario_millones)) +
geom_boxplot(fill = "#00529B", alpha = 0.85) +
labs(x = "salario (millones de $)", y = NULL,
title = "Boxplot: mediana, cuartiles, bigotes")
p2 <- ggplot(emp_edu, aes(y = nivel, x = salario_millones)) +
geom_violin(fill = "#00529B", alpha = 0.85) +
labs(x = "salario (millones de $)", y = NULL,
title = "Violin: ademas, la FORMA de la distribucion")
# La variable ordinal va con barras, una por cada valor que existe.
# El porcentaje se calcula DENTRO de cada grupo: los dos grupos suman 100 por separado.
intervencion <- bien |>
filter(fase == "intervencion") |>
count(grupo_fruta, bienestar) |>
group_by(grupo_fruta) |>
mutate(porcentaje = 100 * n / sum(n)) |>
ungroup()
p3 <- ggplot(intervencion, aes(x = bienestar, y = porcentaje, fill = grupo_fruta)) +
geom_col(position = position_dodge(preserve = "single")) +
scale_x_continuous(breaks = 1:7) +
labs(x = "bienestar (1 a 7)", y = "% dentro del grupo", fill = "grupo",
title = "Una escala de 1 a 7 va con barras")
# El & de patchwork aplica un ajuste a TODOS los paneles de la hoja: aca, achicar
# el titulo de cada uno para que los tres entren sin pisarse.
options(repr.plot.width = 13.5, repr.plot.height = 3.8)
p <- (p1 | p2 | p3) & theme(plot.title = element_text(size = 10))
ggsave("figuras/clase03_box_violin_r.png", p, dpi = 150, width = 13.5, height = 3.8, bg = "white")
p
El boxplot resume tan bien que esconde cuánta gente hay detrás. No es razón para no usarlo (lo vamos a usar mucho), es razón para chequear los n antes de mandarlo al informe, y si hay celdas chicas, decirlo o superponer los puntos.
Un ejemplo para cerrar: la línea que afirma de más
El gráfico de líneas es cómodo, y muchas herramientas lo ofrecen por default incluso cuando el mapeo no lo admite. ggplot2 es de las que se resisten un poco: para unir categorías con una línea hay que insistirle con group = 1. Pero si uno insiste, la hace.
# Contraejemplo: unir con una linea categorias que no tienen orden.
medias_area <- bien |>
group_by(area) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
options(repr.plot.width = 7, repr.plot.height = 3.6)
ggplot(medias_area, aes(x = area, y = bienestar, group = 1)) +
geom_line(colour = "#00529B", linewidth = 0.8) +
geom_point(colour = "#00529B", size = 2.5) +
labs(x = NULL, y = "bienestar promedio", title = "¿Que afirma esta linea?")
La línea afirma continuidad. Dice que entre Ingeniería y Ventas existen valores intermedios, que uno podría pararse “en la mitad” entre las dos áreas y leer un bienestar. Pero no existen, entre dos categorías no hay nada.
De ahí la regla: se unen puntos con una línea sólo cuando el eje x es tiempo o alguna otra cantidad continua. Con categorías, puntos o barras.
Relaciones x-y: un punto por persona
La línea de recién falló porque el eje x era una categoría. Cuando las dos variables son cantidades, se abre otra rama del catálogo, la de relaciones, y su gráfico básico es el scatterplot (o gráfico de dispersión): el que contesta la pregunta “¿estos dos números van juntos?”.
RRHH dejó una de esas preguntas dando vueltas: ¿el salario acompaña a la edad? Cada punto de la figura de abajo es un empleado, con su edad en el eje x y su sueldo en el eje y.
# Dos cantidades, una en cada eje: un punto por empleado.
# alpha = 0.5 los dibuja semitransparentes: son 600 y se pisan entre si.
options(repr.plot.width = 7, repr.plot.height = 4)
ggplot(emp, aes(x = edad, y = salario_millones)) +
geom_point(alpha = 0.5, colour = "#00529B") +
labs(x = "edad (años)", y = "salario mensual (millones de $)",
title = "Edad y salario: un punto por empleado")
El scatterplot no resume nada. Muestra dos cosas a la vez, y conviene leerlas por separado.
La primera es la forma de la relación: el sueldo sube entre los veinte y los cuarenta, después se aplana, y pasados los cincuenta parece aflojar un poco. No es una recta, y con esta nube tampoco se puede afirmar mucho más que eso.
La segunda es la dispersión: entre los empleados de 45 años los hay ganando 1,10 millones y los hay ganando 1,43. Esa distancia, dentro de una misma edad parece bastante grande. Una barra con el salario promedio por franja etaria habría contado lo primero y tapado lo segundo por completo.
Y la regla de mapeo es la de la línea, dada vuelta: scatterplot cuando las dos variables son cantidades. Si una de las dos es una categoría, la comparación vuelve a la familia de barras, boxplots y violines.
✏️ Consigna 1: el gráfico que contesta cada pregunta
RRHH manda cuatro preguntas por mail. Completá la geometría de ggplot2 que contesta cada una (el resto del código ya está armado):
- “¿Cuánta gente tiene cada área?”
- “¿El bienestar se corrió entre fases en cada grupo?”
- “¿Los que llevan más años ganan más?”
- “¿Cómo se distribuye el salario?”
# TODO: completa la geometria de ggplot2 que contesta cada pregunta
orden_areas <- empleados |> count(area, sort = TRUE) |> pull(area)
p1 <- ggplot(empleados, aes(y = factor(area, levels = rev(orden_areas)))) +
geom_____(fill = "#00529B") +
labs(x = "empleados", y = NULL, title = "1. gente por area")
p2 <- ggplot(bien, aes(x = fase, y = bienestar, fill = grupo_fruta)) +
geom_____() +
labs(x = NULL, fill = "grupo", title = "2. bienestar por fase y grupo")
p3 <- ggplot(emp, aes(x = antiguedad_anios, y = salario_millones)) +
geom_____(alpha = 0.5, colour = "#00529B") +
labs(x = "antigüedad (años)", y = "salario (millones de $)",
title = "3. antiguedad vs salario")
p4 <- ggplot(emp, aes(x = salario_millones)) +
geom_____(fill = "#00529B", colour = "white") +
labs(x = "salario (millones de $)", y = "empleados",
title = "4. distribucion del salario")
options(repr.plot.width = 11, repr.plot.height = 6.5)
(p1 | p2) / (p3 | p4)4. Las decisiones que cambian la historia
Hasta acá elegimos qué graficar. Ahora, las decisiones que cambian lo que el lector concluye aunque el gráfico no cambie de tipo. Dos gráficos correctos del mismo dato pueden contar historias distintas, y el informe obliga a elegir uno.
La relación de aspecto
serie <- bien |>
group_by(semana) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
# La MISMA serie, tres hojas de forma distinta. Solo cambia el tamano de la hoja
# (ancho, alto): ni los datos ni los ejes ni las etiquetas.
figura <- function(titulo) {
ggplot(serie, aes(x = semana, y = bienestar)) +
geom_line(colour = "#00529B", linewidth = 0.8) +
geom_point(colour = "#00529B", size = 2) +
scale_x_continuous(breaks = 1:8) +
labs(x = NULL, y = NULL, title = titulo)
}
p_ancha <- figura("ancha y baja: 'apenas se movio'")
options(repr.plot.width = 10, repr.plot.height = 1.8); print(p_ancha)
ggsave("figuras/clase03_aspecto_ancho_r.png", p_ancha, dpi = 150, width = 10, height = 1.8, bg = "white")
p_cuad <- figura("cuadrada: neutral")
options(repr.plot.width = 4, repr.plot.height = 4); print(p_cuad)
ggsave("figuras/clase03_aspecto_cuadrado_r.png", p_cuad, dpi = 150, width = 4, height = 4, bg = "white")
p_alta <- figura("angosta y alta, 'salto enorme'")
options(repr.plot.width = 2.4, repr.plot.height = 4.5); print(p_alta)
ggsave("figuras/clase03_aspecto_alto_r.png", p_alta, dpi = 150, width = 2.4, height = 4.5, bg = "white")
# Proba: cambia los width/height y volve a correr.


¿Cuál de las tres está mal? Ninguna. Son los mismos ocho números, con los mismos ejes y las mismas etiquetas: las tres son visualizaciones válidas de los mismos datos. Y sin embargo la primera sugiere que no pasó nada y la tercera sugiere un salto dramático.
Acá aparece el hilo de la clase en su forma más pura, pero cuidado con la interpretación más laxa del problema. Wilke no dice que cada uno dibuje la historia que quiera. Dice que varias versiones son igualmente válidas, y que la elección se justifica por qué diferencias importa hacer notar. Es una libertad de elección, pero que debe hacerse a conciencia.
El color: tres usos, tres familias de escala
“Hay tres usos fundamentales del color en visualización de datos: (i) podemos usar color para distinguir grupos de datos entre sí; (ii) podemos usar color para representar valores; y (iii) podemos usar color para destacar.” (Wilke, cap. 4)
Cada uso pide su familia de colores:
- cualitativa para distinguir grupos sin orden,
- secuencial para representar valores,
- de acento para destacar.
Usar la familia equivocada nos da un mal gráfico, no es un problema de estética.
Un mal uso por familia:
# Distinguir MAL: una escala secuencial sobre una variable sin orden.
# count() cuenta filas por categoria y devuelve una tabla con "area" y "n".
conteo <- empleados |>
count(area, sort = TRUE) |>
mutate(area = factor(area, levels = rev(area))) # el primer nivel va abajo
options(repr.plot.width = 8.5, repr.plot.height = 3.4)
ggplot(conteo, aes(x = n, y = area, fill = area)) +
geom_col() +
scale_fill_brewer(palette = "Blues", direction = -1, guide = "none") +
labs(x = "empleados", y = NULL,
title = "¿Cual area 'es mas'? El azul oscuro insinua un orden que no existe")
Si alguien pensó RRHH, aunque sea un momento, es entendible: una escala secuencial le inventó una jerarquía a una variable que no la tiene. Las áreas no van de menos a más; son cinco categorías sin orden. Pero el azul oscuro se lee como “más” y el claro como “menos”, así que el gráfico está afirmando algo sobre los datos que los datos no dicen.
Es malo, no feo: el problema no es que combine mal, es que se percibe mal. Para variables nominales va una paleta cualitativa, donde ningún color pesa más que otro.
# Representar MAL: una escala divergente desbalanceada.
media_global <- mean(bien$bienestar)
desvio <- bien |>
group_by(sede, semana) |>
summarise(desvio = mean(bienestar) - media_global, .groups = "drop") |>
mutate(sede = factor(sede, levels = rev(sort(unique(sede)))))
# gradientn reparte los tres colores en partes iguales entre los limites: donde
# caiga el blanco depende de esos limites, y ahi esta el problema de la de la
# derecha. oob = squish recorta lo que se sale del rango en vez de dejarlo gris.
heatmap_desvio <- function(limites, titulo) {
ggplot(desvio, aes(x = factor(semana), y = sede, fill = desvio)) +
geom_tile() +
scale_fill_gradientn(colours = c("#2166AC", "#F7F7F7", "#B2182B"),
limits = limites, oob = scales::squish, name = "desvio") +
labs(x = "semana", y = NULL, title = titulo)
}
options(repr.plot.width = 11.5, repr.plot.height = 3.6)
heatmap_desvio(c(-0.45, 0.45), "Balanceada: mismo desvio, misma intensidad") |
heatmap_desvio(c(-0.10, 0.45), "Desbalanceada: -0.1 grita, +0.1 susurra")
En el panel de la derecha, un desvío de −0,1 se ve intenso y uno de +0,1 casi no se ve. Es el mismo número, con el mismo signo cambiado, y el ojo lee dos magnitudes distintas. Esta figura es incorrecta.
Una escala divergente está compuesta por dos escalas secuenciales pegadas en un punto medio, y por eso tiene que estar balanceada: la progresión hacia cada lado debe ser equivalente.
Ojo, lo que justifica su uso no es que el número tenga signo, sino que el punto medio tenga significado propio. Por ejemplo, acá el cero separa “por encima del promedio” de “por debajo”.
# Destacar: gastar el color en UNA sola cosa. Atletas australianos (Telford &
# Cunningham 1991), el ejemplo del libro, con sus datos originales.
ais <- read_csv("https://vincentarelbundock.github.io/Rdatasets/csv/DAAG/ais.csv",
show_col_types = FALSE)
# %in%: ¿el valor esta en esta lista? Devuelve TRUE/FALSE por fila (Clase 2).
varones <- ais |>
filter(sex == "m") |>
mutate(grupo = ifelse(sport %in% c("T_400m", "T_Sprnt"),
"atletismo (pista)", "otros deportes")) |>
arrange(grupo == "atletismo (pista)") # los destacados al final: se dibujan encima
options(repr.plot.width = 7, repr.plot.height = 4)
p <- ggplot(varones, aes(x = ht, y = pcBfat, colour = grupo, size = grupo, alpha = grupo)) +
geom_point() +
scale_colour_manual(values = c("otros deportes" = "#9FB0BD",
"atletismo (pista)" = "#C0492F"), name = NULL) +
scale_size_manual(values = c("otros deportes" = 1.6,
"atletismo (pista)" = 2.8), guide = "none") +
scale_alpha_manual(values = c("otros deportes" = 0.7,
"atletismo (pista)" = 1), guide = "none") +
labs(x = "altura (cm)", y = "% grasa corporal",
title = "Los atletas de pista, entre los mas bajos y magros")
ggsave("figuras/clase03_highlight_atletas_r.png", p, dpi = 150, width = 7, height = 4, bg = "white")
p
¿Cuántos colores tiene esa figura? Dos. ¿Y cuántos grupos hay en los datos? Muchos más.
La figura junta todos los otros deportes en un solo gris. Eso es destacar. Es agotar el color entero en una sola cosa, y mandar todo lo demás al fondo de modo que no compita por la atención.
La conclusión, que los de pista están entre los más bajos y magros, se lee más fácilmente.
# ✏️ ¿Qué familia de escala le corresponde a cada una?
# Opciones: "cualitativa", "secuencial", "divergente"
# 1. Sede
escala_sede <- "elegir"
# 2. Años de antiguedad
escala_antiguedad <- "elegir"
# 3. Bienestar
escala_bienestar <- "elegir"
corregir(list("1. sede", escala_sede, "cualitativa"),
list("2. antiguedad_anios", escala_antiguedad, "secuencial"),
list("3. bienestar", escala_bienestar, "secuencial"))⚪ 1. sede — todavía sin responder
⚪ 2. antiguedad_anios — todavía sin responder
⚪ 3. bienestar — todavía sin responder
✏️ Consigna 2: ¿cuál va al informe?
La consigna central de la clase. Abajo hay tres versiones de la figura principal del piloto. Los números detrás de las tres son los mismos, pero una de las tres ya la auditaste en la sección 1 y sabés cómo se llama: usala de filtro.
Antes de decidir, mirá en qué número arranca el eje y de cada panel.
Elegí cuál mandarías a RRHH y justificalo en dos líneas. Entre las que quedan no hay una única respuesta correcta, pero hay justificaciones que no se sostienen.
# A: las distribuciones completas
pA <- ggplot(bien, aes(x = fase, y = bienestar, fill = grupo_fruta)) +
geom_violin(linewidth = 0.3) +
# el boxplot fino adentro del violin muestra la mediana y los cuartiles.
# aes(group = ...) hace falta porque pintarlo de blanco fijo borra el grupo.
geom_boxplot(aes(group = interaction(fase, grupo_fruta)),
width = 0.09, linewidth = 0.3, outlier.shape = NA,
fill = "white", position = position_dodge(width = 0.9)) +
labs(x = NULL, y = "bienestar (1 a 7)", title = "A: distribuciones completas") +
theme(legend.position = "none")
# B: las medias con su intervalo de confianza del 95%
# (aproximacion normal: media +- 1,96 errores estandar)
medias_ic <- bien |>
group_by(fase, grupo_fruta) |>
summarise(media = mean(bienestar),
ee = sd(bienestar) / sqrt(n()), .groups = "drop") |>
mutate(bajo = media - 1.96 * ee, alto = media + 1.96 * ee)
pB <- ggplot(medias_ic, aes(x = fase, y = media, colour = grupo_fruta)) +
geom_pointrange(aes(ymin = bajo, ymax = alto),
position = position_dodge(width = 0.2)) +
labs(x = NULL, y = "bienestar (1 a 7)", colour = "grupo", title = "B: medias e IC 95%")
# C: las medias como barras
medias_barra <- bien |>
group_by(fase, grupo_fruta) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
pC <- ggplot(medias_barra, aes(x = fase, y = bienestar, fill = grupo_fruta)) +
geom_col(position = "dodge") +
coord_cartesian(ylim = c(4.4, 5.05)) +
labs(x = NULL, y = "bienestar (1 a 7)", title = "C: barras") +
theme(legend.position = "none")
options(repr.plot.width = 12.5, repr.plot.height = 3.8)
p <- pA | pB | pC
ggsave("figuras/clase03_tres_versiones_r.png", p, dpi = 150, width = 12.5, height = 3.8, bg = "white")
p
eleccion <- "__"
justificacion <- "___"
cat("Eleccion:", eleccion, "\n")
cat(justificacion, "\n")Eleccion: __
___
¿Ya elegiste? Corré la celda de abajo para ver los pros y los contras de las tres opciones, y comparar tu justificación.
#@title 🔎 Pros y contras de las tres opciones {display-mode: "form"}
# Los numeros salen de los datos, no estan escritos a mano.
.inter <- medias_barra |> filter(fase == "intervencion")
.trat <- .inter$bienestar[.inter$grupo_fruta == "Tratamiento"]
.ctrl <- .inter$bienestar[.inter$grupo_fruta == "Control"]
DIF_REAL <- 100 * (.trat / .ctrl - 1)
PISO_C <- 4.4 # el coord_cartesian del panel C
DISTORSION <- (.trat - PISO_C) / (.ctrl - PISO_C)
OPCIONES <- list(
A = list("Violinplot",
c("Muestra las distribuciones de los grupos.",
"El eje va de 1 a 7, la escala real."),
c(sprintf("El efecto real (%.1f%%) queda adentro del solape, y el lector puede concluir 'no paso nada'.", DIF_REAL),
"No dice nada sobre las diferencias entre e intra fases.")),
B = list("Pointrange",
c("Muestra el efecto Y su cambio de fase a fase.",
"Un punto dice cuanto con su posición relativa, asi que acotar el eje es solo un zoom."),
c("Resume 24.000 mediciones en cuatro puntos. La noción de grupo se pierde un poco",
"El zoom hace que un tercio de punto ocupe media pantalla. Hay que aclarar el rango del eje en el epigrafe.")),
C = list("Barras",
c("Se lee de un vistazo, es la que mas se ve en presentaciones."),
c(sprintf("Una barra dice cuanto con su longitud total: la de Tratamiento se ve %.1f veces mas alta que la de Control, y la diferencia real es del %.1f%%.", DISTORSION, DIF_REAL),
"Es incorrecta, no fea. Ya la auditaste en la seccion 1.")))
elegida <- toupper(trimws(eleccion))
if (!elegida %in% names(OPCIONES)) {
elegida <- ""
cat("No elegiste A, B ni C. Completa `eleccion` arriba y volve a correr esta celda.\n\n")
}
for (letra in names(OPCIONES)) {
o <- OPCIONES[[letra]]
marca <- if (letra == elegida) " <-- TU ELECCION" else ""
encabezado <- paste0(letra, ": ", o[[1]], marca)
# \033[1m enciende la negrita y \033[0m la apaga.
cat(if (letra == elegida) paste0("\033[1m", encabezado, "\033[0m\n") else paste0(encabezado, "\n"))
for (p in o[[2]]) cat(" + ", p, "\n")
for (c in o[[3]]) cat(" - ", c, "\n")
cat("\n")
}No elegiste A, B ni C. Completa `eleccion` arriba y volve a correr esta celda.
A: Violinplot
+ Muestra las distribuciones de los grupos.
+ El eje va de 1 a 7, la escala real.
- El efecto real (7.4%) queda adentro del solape, y el lector puede concluir 'no paso nada'.
- No dice nada sobre las diferencias entre e intra fases.
B: Pointrange
+ Muestra el efecto Y su cambio de fase a fase.
+ Un punto dice cuanto con su posición relativa, asi que acotar el eje es solo un zoom.
- Resume 24.000 mediciones en cuatro puntos. La noción de grupo se pierde un poco
- El zoom hace que un tercio de punto ocupe media pantalla. Hay que aclarar el rango del eje en el epigrafe.
C: Barras
+ Se lee de un vistazo, es la que mas se ve en presentaciones.
- Una barra dice cuanto con su longitud total: la de Tratamiento se ve 2.6 veces mas alta que la de Control, y la diferencia real es del 7.4%.
- Es incorrecta, no fea. Ya la auditaste en la seccion 1.
Entre A y B no hay una unica respuesta correcta: son dos figuras honestas que contestan preguntas distintas. Con C si hay una respuesta. Y ojo, lo que la hace incorrecta a C NO es que el eje arranque arriba de cero, porque B tambien arranca arriba de cero y esta bien. La diferencia es qué atributo codifica el valor: la posicion admite zoom, la longitud no.
5. Componer y comunicar
El informe no va a tener una figura: va a tener varias. Dos reglas gobiernan cómo se leen en conjunto.
Regla 1: paneles que se comparan comparten escala
Para partir una figura en paneles (uno por área, por sede, por nivel educativo) ggplot2 no pide una función nueva: se le suma una capa más, facet_wrap(~ columna). Un panel por cada valor de esa columna, todos con la misma geometría y todos hechos de una sola vez.
Ojo con no confundirlo con patchwork, que también arma hojas de varios paneles: patchwork pega gráficos distintos, y facet_wrap parte un mismo gráfico por una variable.
Y facet_wrap trae un argumento que decide si esos paneles se pueden comparar entre sí: scales. Miralo dos veces, con un solo argumento de diferencia:
# Un panel por nivel educativo, y cada panel con SU PROPIO eje (scales = "free_y").
orden_educacion <- c("Secundario", "Terciario", "Universitario", "Posgrado")
historico <- empleados |>
inner_join(salario, by = "empleado_id") |>
mutate(salario_millones = salario_mensual / 1e6,
# El nivel educativo tiene un orden natural, y los paneles lo respetan.
nivel = factor(educacion_nivel, levels = orden_educacion))
serie_edu <- historico |>
group_by(nivel, anio) |>
summarise(salario_millones = mean(salario_millones), .groups = "drop")
options(repr.plot.width = 11, repr.plot.height = 3)
ggplot(serie_edu, aes(x = anio, y = salario_millones)) +
geom_line(colour = "#00529B", linewidth = 0.8) +
facet_wrap(~ nivel, nrow = 1, scales = "free_y") +
scale_x_continuous(breaks = c(2023, 2024, 2025)) +
labs(x = NULL, y = "salario (millones de $)",
title = "scales = 'free_y': ¿que nivel educativo gana mas?")
Contestá antes de seguir: ¿qué nivel educativo gana más?
Los cuatro paneles se ven casi iguales, y esa es la trampa: cada uno tiene su propio eje. Con scales = "free_y" ggplot2 estira cada panel hasta llenar su caja, así que los cuatro suben lo mismo en la pantalla sin importar de dónde arrancan.
Wilke lo dice así: la mente del lector espera que los ejes sean los mismos, y compara alturas entre paneles sin verificar las escalas. Con ejes libres esa comparación es directamente inválida, aunque cada panel por separado sea impecable.
# Un solo argumento de diferencia: sin `scales`, vale "fixed" (el default) y los
# cuatro paneles comparten el eje y.
p <- ggplot(serie_edu, aes(x = anio, y = salario_millones)) +
geom_line(colour = "#00529B", linewidth = 0.8) +
facet_wrap(~ nivel, nrow = 1) +
scale_x_continuous(breaks = c(2023, 2024, 2025)) +
labs(x = NULL, y = "salario (millones de $)",
title = "scales = 'fixed' (el default): ahora si se puede comparar")
options(repr.plot.width = 11, repr.plot.height = 3)
ggsave("figuras/clase03_facetas_r.png", p, dpi = 150, width = 11, height = 3, bg = "white")
p
# La brecha de 2025 que cita el texto de abajo, calculada y verificada.
.n25 <- serie_edu |> filter(anio == 2025)
.pos <- .n25$salario_millones[.n25$nivel == "Posgrado"]
.sec <- .n25$salario_millones[.n25$nivel == "Secundario"]
cat(sprintf("2025: Posgrado %.2f millones vs Secundario %.2f millones\n", .pos, .sec))
stopifnot(round(.pos, 2) == 1.36, round(.sec, 2) == 1.13,
abs(100 * (.pos / .sec - 1) - 20) < 1.5)2025: Posgrado 1.36 millones vs Secundario 1.13 millones

Ahora sí, y la conclusión no es la que sugería la figura anterior. Los cuatro niveles suben casi en paralelo, pero arrancan en alturas muy distintas: en 2025 un posgrado promedia 1,36 millones y un secundario 1,13, un 20% de diferencia. Esa brecha, que es la conclusión principal de la figura, era invisible con los ejes libres.
¿Entonces scales = "free_y" está prohibido? No. A veces las magnitudes son tan distintas que compartir eje aplasta todos los paneles menos uno. Pero en ese caso hay que avisarlo explícitamente en el epígrafe, porque el lector no lo va a chequear solo.
Y otra regla del mismo capítulo, que esta figura ya aplica: los paneles van siempre en un orden con sentido. Acá van de Secundario a Posgrado porque el nivel educativo tiene un orden natural, y eso se pide fijando los niveles del factor con factor(..., levels = ...). Dejarlos en el orden alfabético con el que vinieron habría sido, otra vez, una decisión no tomada.
A veces el hallazgo no está en ningún panel, emerge al compararlos. Para ello, primero tienen que ser efectivamente comparables.
Regla 2: el mismo lenguaje visual en todo el informe
Si Tratamiento es terracota en la figura 1, es terracota en todas. Si las sedes van ordenadas por tamaño en una figura, van así en todas. El test rápido: contá las leyendas. Si necesitás dos leyendas para explicar la misma variable, el lenguaje visual no es consistente.
Regla 3: las palabras son parte del mapeo
“Los títulos y etiquetas de ejes y leyendas explican qué son los valores que se muestran y cómo se mapean a los atributos gráficos.” (Wilke, cap. 22)
Las etiquetas son la parte verbal del mapeo: dicen qué significa cada atributo. Se pueden omitir solo cuando las etiquetas de los valores ya lo explican todo (una leyenda “Femenino / Masculino” no necesita el título “género”; una “Control / Tratamiento” sin título es un enigma).
El principio de la cátedra es más exigente que el del libro: el título de la figura es la conclusión, porque el que decide lee el título antes que los ejes.
piloto <- bien |>
group_by(semana, grupo_fruta) |>
summarise(bienestar = mean(bienestar), .groups = "drop")
base <- ggplot(piloto, aes(x = semana, y = bienestar, colour = grupo_fruta)) +
geom_line(linewidth = 0.8) + geom_point(size = 1.8) +
scale_x_continuous(breaks = 1:8)
p1 <- base +
labs(x = NULL, y = NULL, title = "Sin palabras: el lector adivina") +
theme(legend.position = "none")
p2 <- base +
labs(x = "semana del piloto", y = "bienestar promedio (1-7)", colour = "grupo",
title = "El bienestar sube solo en el grupo con fruta")
options(repr.plot.width = 11.5, repr.plot.height = 3.8)
p <- p1 | p2
ggsave("figuras/clase03_titulos_r.png", p, dpi = 150, width = 11.5, height = 3.8, bg = "white")
p
# "It is a bad practice to make your readers guess what you mean" (Wilke).
6. El informe
Última pieza: convertir el análisis en una figura que encabece el informe. El capítulo 29 de Wilke pide contar una historia con estructura de tensión y resolución, y deja dos frases que funcionan como resumen de toda la clase:
“Cuando intentás mostrar demasiados datos a la vez, podés terminar no mostrando nada.” (Wilke, cap. 29)
“Simple y claro le gana a complejo y confuso.” (Wilke, cap. 29)
Primero la versión que intenta mostrarlo todo. Tomate veinte segundos mirándola antes de seguir. ¿Qué conclusión te llevas?
options(repr.plot.width = 8.5, repr.plot.height = 4.2)
ggplot(emp, aes(x = antiguedad_anios, y = salario_millones,
colour = area, size = edad, shape = genero)) +
geom_point(alpha = 0.6) +
labs(x = "antigüedad (años)", y = "salario mensual (millones de $)",
title = "Cinco variables en un grafico. ¿Que conclusion te llevas?") +
theme(legend.text = element_text(size = 7), legend.title = element_text(size = 8))
La respuesta honesta es ninguna. Hay cinco variables mapeadas ahí adentro (posición en x, posición en y, color, tamaño y forma), tres leyendas, y cero historia.
No es necesario graficar todas las dimensiones que uno tiene. Hay que graficar la que responde la pregunta. Y la pregunta de RRHH ya la tenemos.
# La figura del informe: la pregunta de RRHH, contestada y anotada.
# El efecto sale de los datos y entra al titulo: no se escribe a mano.
.fin <- medias_barra |> filter(fase == "intervencion")
efecto <- .fin$bienestar[.fin$grupo_fruta == "Tratamiento"] -
.fin$bienestar[.fin$grupo_fruta == "Control"]
stopifnot(round(efecto, 2) == 0.34)
p <- ggplot(piloto, aes(x = semana, y = bienestar, colour = grupo_fruta)) +
geom_line(linewidth = 0.9) + geom_point(size = 2) +
geom_vline(xintercept = 4.5, colour = "#9FB0BD", linetype = "dashed") + # linea vertical
annotate("text", x = 4.65, y = 4.53, label = "empieza la fruta", # texto encima
colour = "#57708a", size = 3.2, hjust = 0) +
scale_x_continuous(breaks = 1:8) +
labs(x = "semana del piloto", y = "bienestar promedio (escala 1-7)", colour = "grupo",
title = paste0("El bienestar sube solo en el grupo con fruta: +",
sub("\\.", ",", sprintf("%.2f", efecto)),
" puntos, un efecto chico y consistente"))
options(repr.plot.width = 9, repr.plot.height = 4.5)
ggsave("figuras/clase03_figura_informe_r.png", p, dpi = 150, width = 9, height = 4.5, bg = "white")
p
La última decisión: el título
Tres títulos posibles para esa figura. Ninguno contradice los números:
| título | problema |
|---|---|
| “Bienestar por fase y grupo” | describe el contenido, no dice nada |
| “El programa de fruta mejora el bienestar de los empleados” | dice más de lo que el dato sostiene |
| “El bienestar sube solo en el grupo con fruta: un efecto chico y consistente” | el más sobrio |
El segundo es el tentador: es el título que un equipo de datos manda cuando quiere que su proyecto siga vivo. Sostener el matiz (subió, es consistente, es chico) es más difícil que exagerarlo, pero es exactamente el trabajo que hay que hacer.
Y esto era, además, un reporte reproducible
Una última cosa, recordemos que esta notebook es el informe. Tiene el texto, el código que produjo cada número, las figuras y la conclusión. Cualquiera puede volver a correrla de arriba a abajo y obtener lo mismo, y si mañana llegan datos nuevos, se corre de nuevo y las figuras se regeneran. Eso es un reporte reproducible: no un documento sobre el análisis, sino el análisis mismo.
Para mandarlo: en Colab, Archivo → Descargar → .ipynb (el código vivo) o Archivo → Imprimir → PDF (la foto para quien no corre código).
✏️ Consigna bonus track
RRHH quedó conforme y manda otro pedido, esta vez sobre rotación de personal. El dataset es hr_attrition (IBM): 1.470 empleados, 35 columnas, y una columna Attrition que dice si la persona dejó la empresa.
El pedido: una (1) figura que sostenga una afirmación sobre quiénes se van, con título-conclusión, ejes etiquetados y guardada a archivo. Sugerencia de arranque: ¿los que se van ganan distinto (MonthlyIncome) que los que se quedan?
Esta consigna no la vamos a hacer en clase: es el único tramo largo y no entra en los tiempos. Queda como práctica por tu cuenta, con el enunciado y los datos ya listos acá abajo. Es el mejor repaso de todo lo de hoy.
URL_HR <- "https://raw.githubusercontent.com/IBM/employee-attrition-aif360/master/data/emp_attrition.csv"
hr <- read_csv(URL_HR, show_col_types = FALSE)
cat(dim(hr), "\n")
hr |> select(Age, Attrition, Department, MonthlyIncome, JobRole, OverTime) |> head()1470 35
| Age | Attrition | Department | MonthlyIncome | JobRole | OverTime |
|---|---|---|---|---|---|
| <dbl> | <chr> | <chr> | <dbl> | <chr> | <chr> |
| 41 | Yes | Sales | 5993 | Sales Executive | Yes |
| 49 | No | Research & Development | 5130 | Research Scientist | No |
| 37 | Yes | Research & Development | 2090 | Laboratory Technician | Yes |
| 33 | No | Research & Development | 2909 | Research Scientist | Yes |
| 27 | No | Research & Development | 3468 | Laboratory Technician | No |
| 32 | No | Research & Development | 3068 | Laboratory Technician | No |
medianas <- hr |>
group_by(Attrition) |>
summarise(mediana = median(MonthlyIncome), .groups = "drop")
print(medianas)
# TODO: elegi la geometria Y el mapeo que sostienen tu afirmacion
# (pista: horizontal se lee mejor cuando las etiquetas son texto, como en la seccion 1)
p <- ggplot(hr, aes(y = ___, x = ___)) +
geom_____() +
# TODO: el titulo es la conclusion, no la descripcion
labs(x = "___", y = "___", title = "___")
options(repr.plot.width = 8, repr.plot.height = 3.5)
ggsave("figuras/clase03_cierre_attrition_r.png", p, dpi = 150, width = 8, height = 3.5, bg = "white")
pResumen de hoy
| Paso | Qué usamos | En una línea |
|---|---|---|
| El criterio | feo / malo / incorrecto | estética, percepción o matemática: tres fallas distintas, tres gravedades |
| El mapeo | aes(x =, y =, colour =, fill =, shape =, size =) |
un gráfico es un mapeo de datos a atributos; la escala debe ser uno a uno |
| El repertorio | geom_bar, geom_histogram, geom_boxplot, geom_violin, geom_point, geom_tile |
primero la pregunta, después el gráfico |
binwidth =, levels = en factor() |
los defaults deciden por vos: el bin, el orden de las categorías | |
| Las decisiones | scale_fill_brewer, scale_fill_viridis_c, scale_fill_gradient2 |
cada uso del color pide su familia; el highlight gasta el color en una sola cosa |
| Componer | facet_wrap(scales =), p1 | p2 de patchwork |
paneles que se comparan comparten escala; mismo lenguaje visual en todo el informe |
| Comunicar | labs(title =, x =, y =), ggsave() |
las palabras son parte del mapeo; el título es la conclusión |
Mini apéndice para cuando lo necesites: la galería de ggplot2 + from Data to Viz como repertorio de consulta.
Lo que nos llevamos hoy: dos gráficos correctos del mismo dato pueden contar historias distintas. Elegir cuál mandar es una decisión de comunicación con consecuencias, y esa decisión, como las de la Clase 2, debe ser a conciencia.