toupper("hola")[1] "HOLA"
Photo by Click and Boo on Unsplash

En este ejercicio, presentaré y explicaré todo el código necesario para reproducir el Gráfico A visto arriba.
El reto, sin embargo, consiste en desarrollar el código de manera que sirva como una puerta de ingreso para personas que nunca han programado en R ni en ningún otro lenguaje.
No hace falta instalar R
No es necesario instalar ningún programa para completar este ejercicio. Se puede acceder a RStudio en línea mediante una cuenta de Posit Cloud.
Otra opción es utilizar el acceso específico para R de Google Colab. Aunque Colab está orientado principalmente a Python, este enlace abre un entorno de ejecución de R y permite ejecutar el código sin instalar R ni RStudio en la computadora.
Programar en R consiste en aplicar funciones a objetos.
Por ejemplo, la función toupper() la aplicamos sobre el objeto "hola" y lo convertimos en "HOLA":
toupper("hola")[1] "HOLA"
La función sqrt() la aplicamos sobre el objeto 4 y obtenemos su raíz cuadrada, 2:
sqrt(4)[1] 2
Es una simplificación, pero es útil: en R, programar consiste en aplicar funciones a objetos.
En los ejemplos anteriores aplicamos funciones a un objeto nada más, pero también podemos aplicar funciones a varios objetos simultáneamente.
Para aplicar funciones a varios objetos al mismo tiempo, primero concatenamos todos los objetos mediante la función c():
c("hola", "hello", "olá", "hallo")[1] "hola" "hello" "olá" "hallo"
c(1, -100, 4, -55, 10)[1] 1 -100 4 -55 10
La función c() agrupa objetos individuales en un objeto que en R conocemos como vector.
De modo que concatenamos los objetos individuales y los pasamos todos juntos a la función.
Por ejemplo:
toupper(c("hola", "hello", "olá", "hallo"))[1] "HOLA" "HELLO" "OLÁ" "HALLO"
abs(c(1, -100, 4, -55, 10))[1] 1 100 4 55 10
Como muestra el resultado, R aplica la función a cada elemento de manera individual.
En estos dos ejemplos lo que hicimos fue anidar dos funciones: la función c() quedó dentro de la función toupper(); la función c() quedó dentro de la función abs().
Las funciones anidadas R las ejecuta de adentro hacia afuera, o sea, primero ejecuta c() y el output de dicha función se convierte en el input de toupper().
Anidar funciones puede hacer que el código sea más difícil de leer, especialmente cuando se combinan muchas.
Con el operador |> podemos conseguir lo mismo y a la vez lograr que el código sea más claro:
c("hola", "hello", "olá", "hallo") |>
toupper()[1] "HOLA" "HELLO" "OLÁ" "HALLO"
c(1, -100, 4, -55, 10) |>
abs()[1] 1 100 4 55 10
El operador |> permite seguir con mayor claridad el flujo del código: cuál función está tomando como input el output de la función anterior.
La utilidad del operador |> se vuelve más evidente a medida que encadenamos más funciones:
c() concatena los objetos.
toupper() los convierte en mayúsculas.
sort() los ordena.
[1] "HALLO" "HELLO" "HOLA" "OLÁ"
c() concatena los objetos.
abs() los convierte en valores absolutos.
sort() los ordena.
max() identifica al mayor.
[1] 100
En R, las funciones suelen tener nombres que permiten anticipar fácilmente lo que hacen.
Recapitulemos:
En R hay funciones y objetos.
Programar en R consiste en aplicar funciones a objetos para transformarlos.
La función c() combina varios elementos individuales para crear un vector.
Los vectores son objetos; por tanto, podemos aplicar funciones a vectores.
La siguiente lección es que las funciones tienen argumentos.
Podemos modificar los argumentos de una función para ajustar su comportamiento a las necesidades de cada tarea.
Comentarios directos en el código
En R, todo lo que sigue al símbolo # en una línea se interpreta como un comentario y no se ejecuta.
Los comentarios bien elegidos ayudan a explicar el código y facilitan su lectura, comprensión y mantenimiento.
La función seq(), que permite crear secuencias numéricas, ofrece un ejemplo sencillo para entender cómo funcionan los argumentos en R:
seq(
from = 10, # La secuencia inicia en este número
to = 20 # Y termina en este otro número
) [1] 10 11 12 13 14 15 16 17 18 19 20
La función seq() tiene dos argumentos, from y to, y esos argumentos los podemos manipular para crear cualquier secuencia de números:
En from especificamos con cuál número empieza la secuencia.
En to especificamos con cuál número termina la secuencia.
No es necesario indicar los nombres de los argumentos:
seq(10, 20) [1] 10 11 12 13 14 15 16 17 18 19 20
En el ejemplo anterior, no escribimos from = ni tampoco to =. Tan sólo indicamos 10 y 20, sin decir explícitamente cuál correspondía a from y cuál a to.
Cuando no se especifican los nombres, R asigna los valores a los argumentos según su orden predeterminado.
El orden de los argumentos se puede consultar en la documentación de cada función
Para consultar la documentación de la función seq() basta con ejecutar el shortcut ?seq(). Inmediatamente se desplegará la pestaña Help: en la sección Usage está el orden asignado por defecto a los argumentos, y en la sección siguiente, Arguments, una descripción de cada uno de ellos.
Una de las principales fortalezas de R es, precisamente, la excelente calidad de su documentación.
Cuando indicamos explícitamente el nombre de cada argumento, el orden deja de importar:
seq(from = 10, to = 20) [1] 10 11 12 13 14 15 16 17 18 19 20
seq(to = 20, from = 10) [1] 10 11 12 13 14 15 16 17 18 19 20
Para facilitar la lectura, conviene evitar las líneas de código demasiado largas. Se puede colocar cada argumento en una línea distinta:
seq(
from = 1,
to = 21,
by = 3
)[1] 1 4 7 10 13 16 19
R permite dividir las líneas de código dentro de los paréntesis y después de las comas. Utilizaremos este recurso con frecuencia a lo largo del ejercicio.
El operador : ofrece otra forma de crear secuencias numéricas. Conviene conocerlo desde ahora, pues lo utilizaremos más adelante:
55:60[1] 55 56 57 58 59 60
R incluye una gran cantidad de funciones. Ya hemos utilizado algunas: toupper(), sqrt(), c(), sort(), seq().
Una de las grandes fortalezas de R es su ecosistema de paquetes, que amplía considerablemente las funciones disponibles. Su variedad permite abordar tareas de muy distinta naturaleza sin tener que desarrollar cada herramienta desde cero.
En este ejercicio utilizaremos tidyverse, una colección que reúne varios paquetes de R. Una sola instalación nos permite acceder, entre otros, a los siguientes:
dplyr: paquete especializado en manipulación de datos.
ggplot2: paquete especializado en visualización de datos.
Para poder utilizar las funciones de estos paquetes, debemos completar dos pasos.
El primero es instalar tidyverse mediante la función install.packages():
install.packages("tidyverse")Una vez instalado, el segundo paso es cargar tidyverse con la función library():
library(tidyverse)Para utilizar un paquete, primero debemos instalarlo y luego cargarlo. La instalación se realiza una sola vez, mientras que la carga debe repetirse al iniciar una nueva sesión de R.
Aunque un paquete ya esté instalado en la computadora, no podremos utilizar sus funciones hasta cargarlo en la sesión actual de R:
library(tidyverse)
numbers <- c(2, 5, 8)
between(numbers, 3, 7)[1] FALSE TRUE FALSE
between() es una función del paquete dplyr. En este ejercicio, cuando utilicemos funciones que no forman parte de R base, indicaremos el paquete mediante la sintaxis paquete::función():
dplyr::between(numbers, 3, 7)[1] FALSE TRUE FALSE
:: es un operador conocido como namespace. Permite indicar a qué paquete pertenece una función, pero podemos omitirlo cuando el paquete ya está cargado:
between(numbers, 3, 7)[1] FALSE TRUE FALSE
Incluso cuando el paquete ya está cargado, el operador :: ayuda a evitar ambigüedades si dos paquetes contienen funciones con el mismo nombre. Un ejemplo típico es filter(), disponible como dplyr::filter() y stats::filter().
¡Listo! Con esta primera aproximación a R, incluso quienes nunca han programado cuentan ya con las herramientas básicas para avanzar a las siguientes secciones.
Comencemos por preparar los datos necesarios para este ejercicio.
El data set abarca los torneos disputados en la liga profesional de fútbol masculino de Costa Rica, desde el torneo Invierno 2013 hasta el torneo Clausura 2024. Para cada certamen, registra el número de títulos que había acumulado cada uno de los tres equipos hasta ese momento:
data <- dplyr::tribble(
~torneo, ~Saprissa, ~LDA, ~CSH,
"2013 Invierno", 29, 29, 23, # LDA gana 29
"2014 Verano", 30, 29, 23, # Saprissa gana 30
"2014 Invierno", 31, 29, 23, # Saprissa gana 31
"2015 Verano", 31, 29, 24, # CSH gana 24
"2015 Invierno", 32, 29, 24, # Saprissa gana 32
"2016 Verano", 32, 29, 25, # CSH gana 25
"2016 Invierno", 33, 29, 25, # Saprissa gana 33
"2017 Verano", 33, 29, 26, # CSH gana 26
"2017 Apertura", 33, 29, 26, # Ninguno lo gana
"2018 Clausura", 34, 29, 26, # Saprissa gana 34
"2018 Apertura", 34, 29, 27, # CSH gana 27
"2019 Clausura", 34, 29, 27, # Ninguno lo gana
"2019 Apertura", 34, 29, 28, # CSH gana 28
"2020 Clausura", 35, 29, 28, # Saprissa gana 35
"2020 Apertura", 35, 30, 28, # LDA gana 30
"2021 Clausura", 36, 30, 28, # Saprissa gana 36
"2021 Apertura", 36, 30, 29, # CSH gana 29
"2022 Clausura", 36, 30, 29, # Ninguno lo gana
"2022 Apertura", 37, 30, 29, # Saprissa gana 37
"2023 Clausura", 38, 30, 29, # Saprissa gana 38
"2023 Apertura", 39, 30, 29, # Saprissa gana 39
"2024 Clausura", 40, 30, 29 # Saprissa gana 40
)El código anterior crea un objeto de tipo tibble con los datos que visualizaremos en la siguiente sección.
El origen de los datos
Por lo general, los datos se cargan desde un archivo existente, como una hoja de cálculo de Microsoft Excel. Sin embargo, para simplificar este ejercicio, los introduje manualmente y los organicé en forma de tabla con la función dplyr::tribble(). Los conteos los obtuve de Wikipedia.
Por ahora, no necesitamos detenernos en cómo funciona tribble(). Basta con saber que <- es el operador de asignación. En este caso, guarda los datos generados por la función en un objeto al que hemos llamado data.
Una vez ejecutado ese bloque de código, el objeto data queda almacenado en la memoria de la sesión de R. A partir de entonces, podemos manipular los datos con solo referirnos al objeto por su nombre: data.
Por ejemplo, como data es una tabla, podemos averiguar cuántas filas contiene aplicándole la función nrow():
nrow(data)[1] 22
Para mostrar los datos en la consola, usamos la función print():
print(data)# A tibble: 22 × 4
torneo Saprissa LDA CSH
<chr> <dbl> <dbl> <dbl>
1 2013 Invierno 29 29 23
2 2014 Verano 30 29 23
3 2014 Invierno 31 29 23
4 2015 Verano 31 29 24
5 2015 Invierno 32 29 24
6 2016 Verano 32 29 25
7 2016 Invierno 33 29 25
8 2017 Verano 33 29 26
9 2017 Apertura 33 29 26
10 2018 Clausura 34 29 26
# ℹ 12 more rows
De manera predeterminada, print() muestra solo diez filas del tibble. Podemos modificar el argumento n para indicar el número de filas que queremos ver:
print(data, n = 3)# A tibble: 22 × 4
torneo Saprissa LDA CSH
<chr> <dbl> <dbl> <dbl>
1 2013 Invierno 29 29 23
2 2014 Verano 30 29 23
3 2014 Invierno 31 29 23
# ℹ 19 more rows
Como acabamos de observar, nrow() indica cuántas filas contiene el objeto data, mientras que print() muestra diez pues n = 10 por defecto.
Al combinar ambas funciones, podemos imprimir la tabla completa:
print(data, n = nrow(data))# A tibble: 22 × 4
torneo Saprissa LDA CSH
<chr> <dbl> <dbl> <dbl>
1 2013 Invierno 29 29 23
2 2014 Verano 30 29 23
3 2014 Invierno 31 29 23
4 2015 Verano 31 29 24
5 2015 Invierno 32 29 24
6 2016 Verano 32 29 25
7 2016 Invierno 33 29 25
8 2017 Verano 33 29 26
9 2017 Apertura 33 29 26
10 2018 Clausura 34 29 26
11 2018 Apertura 34 29 27
12 2019 Clausura 34 29 27
13 2019 Apertura 34 29 28
14 2020 Clausura 35 29 28
15 2020 Apertura 35 30 28
16 2021 Clausura 36 30 28
17 2021 Apertura 36 30 29
18 2022 Clausura 36 30 29
19 2022 Apertura 37 30 29
20 2023 Clausura 38 30 29
21 2023 Apertura 39 30 29
22 2024 Clausura 40 30 29
Es oportuno introducir tres aclaraciones sobre data, el data set que acabamos de imprimir en su totalidad:
data registra cuántos títulos había acumulado cada uno de los tres equipos seleccionados (Deportivo Saprissa, Liga Deportiva Alajuelense y Club Sport Herediano) al finalizar cada torneo. Cada fila es un torneo distinto.
data incluye el Clausura 2024, que aún no había comenzado cuando se publicó originalmente este ejercicio, y plantea un escenario hipotético en el que el Deportivo Saprissa gana el torneo. Este es el escenario representado en el Gráfico A, cuya reproducción constituye nuestro objetivo.
data esconde una historia: cuántos títulos de ventaja tendría el Deportivo Saprissa sobre la Liga Deportiva Alajuelense si ganara el Clausura 2024, después de haber conquistado el Apertura 2023, y cómo la distancia entre ambos se amplió de manera dramática en apenas una década. Esta es la historia que comunica el Gráfico A.
Por cierto, si necesitamos contar las columnas de data en lugar de las filas, podemos sustituir nrow() por ncol():
ncol(data)[1] 4
La función dim() roporciona ambas dimensiones de la tabla; primero el número de filas y luego el de columnas:
dim(data)[1] 22 4
Podemos guardar el resultado anterior con el ya conocido operador <-, por ejemplo, asignando ese objeto al nombre dimensiones_originales.
Recordemos que la asignación hecha mediante <- guarda el resultado como un objeto y lo deja disponible durante la sesión de R.
La asignación guarda el objeto, pero no muestra su contenido. Por eso, al ejecutarla, la consola no devuelve ningún resultado visible:
dimensiones_originales <- dim(data)Podemos mostrar el contenido del objeto de dos maneras. La primera es escribir su nombre y ejecutar la línea de código:
dimensiones_originales[1] 22 4
La segunda es utilizando la conocida función print():
print(dimensiones_originales)[1] 22 4
Podemos asignar un objeto a un nombre para guardarlo y al mismo imprimirlo si encerramos toda la expresión entre paréntesis:
(dimensiones_originales <- dim(data))[1] 22 4
Ya tenemos listos los datos. Ahora vamos a modificarlos un poco.
En particular, necesitamos realizar un cambio importante en la estructura de los datos.
Recordemos que, en este momento, data tiene una columna Saprissa, una columna LDA y una columna CSH.
Mostremos algunas filas para recordar cómo están organizados los datos. Las funciones head() y tail() muestran, respectivamente, las primeras y las últimas filas de una tabla:
## Las primeras filas
head(data)# A tibble: 6 × 4
torneo Saprissa LDA CSH
<chr> <dbl> <dbl> <dbl>
1 2013 Invierno 29 29 23
2 2014 Verano 30 29 23
3 2014 Invierno 31 29 23
4 2015 Verano 31 29 24
5 2015 Invierno 32 29 24
6 2016 Verano 32 29 25
## Las últimas filas
tail(data)# A tibble: 6 × 4
torneo Saprissa LDA CSH
<chr> <dbl> <dbl> <dbl>
1 2021 Apertura 36 30 29
2 2022 Clausura 36 30 29
3 2022 Apertura 37 30 29
4 2023 Clausura 38 30 29
5 2023 Apertura 39 30 29
6 2024 Clausura 40 30 29
Para crear la visualización, debemos reorganizar los datos: en lugar de una columna para cada equipo, tendremos una única columna, equipo, con tres posibles valores: Saprissa, LDA y CSH.
Esta reorganización facilita la visualización porque ggplot2 espera que cada variable ocupe una columna. Al reunir los nombres de los equipos en equipo y sus títulos en otra columna, titulos, podemos asignar esas variables directamente a propiedades del gráfico, como el color, el grupo y la posición.
Realizaremos esta transformación mediante las funciones pivot_longer() y mutate(). La primera pertenece a tidyr y la segunda, a dplyr, dos de los paquetes incluidos en tidyverse.
Veamos cómo funciona esta transformación, paso a paso:
1df <- data |>
2 tidyr::pivot_longer(
3 cols = c("Saprissa", "LDA", "CSH"),
4 names_to = "equipo",
5 values_to = "titulos"
) |>
dplyr::mutate(
6 equipo = factor(
equipo,
levels = c("Saprissa", "LDA", "CSH")
)
)df, para guardarlo en la memoria de la sesión.
Saprissa, LDA y CSH.
equipo, que contendrá los nombres de las columnas seleccionadas.
titulos, que contendrá los valores almacenados en esas columnas.
equipo, un paso necesario para especificar el orden de los equipos (Saprissa > LDA > CSH). No necesitamos profundizar todavía en el funcionamiento de los factores en R. Por ahora, basta con saber que nos permitirán controlar el orden en que aparecerán los equipos en la visualización.
En síntesis, transformamos los datos originales y almacenamos el resultado en un nuevo objeto: df.
data y df tienen ahora estructuras distintas. Esta es la clave. Aunque contienen la misma información, la organizan de maneras distintas.
La importancia de conservar los datos originales
Antes de transformar un data frame, conviene guardar una copia intacta de su versión original. Por eso, en lugar de sobrescribir data, almacenamos el resultado de la transformación en un nuevo objeto llamado df.
Los data frames data y df tienen un propósito distinto:
data conserva los datos tal como eran originalmente y nos permite recuperarlos o consultarlos cuando sea necesario.
df funciona como nuestra versión de trabajo, sobre la cual realizaremos las transformaciones necesarias.
Los datos en df tienen la siguiente estructura:
print(df, n = nrow(df))# A tibble: 66 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2013 Invierno Saprissa 29
2 2013 Invierno LDA 29
3 2013 Invierno CSH 23
4 2014 Verano Saprissa 30
5 2014 Verano LDA 29
6 2014 Verano CSH 23
7 2014 Invierno Saprissa 31
8 2014 Invierno LDA 29
9 2014 Invierno CSH 23
10 2015 Verano Saprissa 31
11 2015 Verano LDA 29
12 2015 Verano CSH 24
13 2015 Invierno Saprissa 32
14 2015 Invierno LDA 29
15 2015 Invierno CSH 24
16 2016 Verano Saprissa 32
17 2016 Verano LDA 29
18 2016 Verano CSH 25
19 2016 Invierno Saprissa 33
20 2016 Invierno LDA 29
21 2016 Invierno CSH 25
22 2017 Verano Saprissa 33
23 2017 Verano LDA 29
24 2017 Verano CSH 26
25 2017 Apertura Saprissa 33
26 2017 Apertura LDA 29
27 2017 Apertura CSH 26
28 2018 Clausura Saprissa 34
29 2018 Clausura LDA 29
30 2018 Clausura CSH 26
31 2018 Apertura Saprissa 34
32 2018 Apertura LDA 29
33 2018 Apertura CSH 27
34 2019 Clausura Saprissa 34
35 2019 Clausura LDA 29
36 2019 Clausura CSH 27
37 2019 Apertura Saprissa 34
38 2019 Apertura LDA 29
39 2019 Apertura CSH 28
40 2020 Clausura Saprissa 35
41 2020 Clausura LDA 29
42 2020 Clausura CSH 28
43 2020 Apertura Saprissa 35
44 2020 Apertura LDA 30
45 2020 Apertura CSH 28
46 2021 Clausura Saprissa 36
47 2021 Clausura LDA 30
48 2021 Clausura CSH 28
49 2021 Apertura Saprissa 36
50 2021 Apertura LDA 30
51 2021 Apertura CSH 29
52 2022 Clausura Saprissa 36
53 2022 Clausura LDA 30
54 2022 Clausura CSH 29
55 2022 Apertura Saprissa 37
56 2022 Apertura LDA 30
57 2022 Apertura CSH 29
58 2023 Clausura Saprissa 38
59 2023 Clausura LDA 30
60 2023 Clausura CSH 29
61 2023 Apertura Saprissa 39
62 2023 Apertura LDA 30
63 2023 Apertura CSH 29
64 2024 Clausura Saprissa 40
65 2024 Clausura LDA 30
66 2024 Clausura CSH 29
Nótese las columnas equipo y titulos, columnas que no existen en data.
La transformación modifica la estructura, pero no la información: en comparación con data, df distribuye los mismos datos en menos columnas y más filas.
Comparemos las dimensiones antes y después de la transformación. Las dimensiones originales eran las siguientes:
dimensiones_originales # Filas y columnas, en ese orden[1] 22 4
Las nuevas dimensiones son:
dim(df)[1] 66 3
En otras palabras, la transformación convirtió un data frame ancho, data (22 filas y 4 columnas), en uno largo, df (66 filas y 3 columnas).
Las tres columnas originales, Saprissa, LDA y CSH,se combinaron en una nueva columna llamada equipo. Sus valores se trasladaron a la columna titulos, que indica cuántos títulos había acumulado cada equipo al finalizar cada torneo.
Tidy data
La estructura que hemos dado a df responde a los principios de tidy data: cada variable ocupa una columna y cada observación ocupa una fila.
En nuestro caso, torneo, equipo y titulos son variables distintas, mientras que cada fila representa una observación: el número de títulos acumulados por un equipo al finalizar un torneo determinado.
Esta estructura facilita la visualización porque ggplot2 puede vincular cada columna con una propiedad del gráfico, como la posición, el color o el grupo.
No profundizaremos aquí en los fundamentos de este enfoque. Se puede consultar una explicación técnica y detallada en el artículo Tidy Data, de Hadley Wickham.
Recordemos que df plantea un escenario hipotético en el que Saprissa gana el Clausura 2024. Para examinar las observaciones correspondientes, utilizaremos el operador [], cuya sintaxis permite seleccionar filas y columnas a partir de sus índices.
Cuando utilizamos el operador [], indicamos antes de la coma los índices de las filas que queremos seleccionar y, después de la coma, los índices de las columnas:
## Filas de la 61 a la 63
## Columnas de la 1 a la 3
df[61:63, 1:3]# A tibble: 3 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2023 Apertura Saprissa 39
2 2023 Apertura LDA 30
3 2023 Apertura CSH 29
Si dejamos vacío el espacio después de la coma, R asumirá que queremos seleccionar todas las columnas:
## Filas de la 1 a la 3
## Todas las columnas
df[1:3, ]# A tibble: 3 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2013 Invierno Saprissa 29
2 2013 Invierno LDA 29
3 2013 Invierno CSH 23
Y si dejamos vacío el espacio antes de la coma, R conservará todas las filas:
## Todas las filas, columna 1
df[, 1] # A tibble: 66 × 1
torneo
<chr>
1 2013 Invierno
2 2013 Invierno
3 2013 Invierno
4 2014 Verano
5 2014 Verano
6 2014 Verano
7 2014 Invierno
8 2014 Invierno
9 2014 Invierno
10 2015 Verano
# ℹ 56 more rows
R mostró únicamente 10 filas. Recordemos que el data frame contiene ahora muchas más filas porque lo transformamos a formato largo. Esa era, justamente, la finalidad de la transformación.
Si especificamos una sola fila y una sola columna, es como proporcionar a R las coordenadas exactas de una casilla específica:
df[10, 1]# A tibble: 1 × 1
torneo
<chr>
1 2015 Verano
La casilla correspondiente al título número 40 del Deportivo Saprissa, que en nuestro escenario hipotético ganaría en el Clausura 2024, se extrae así:
df[64, 3]# A tibble: 1 × 1
titulos
<dbl>
1 40
La indexación no solo permite consultar valores, sino también modificarlos.
Imaginemos un escenario alternativo: Saprissa no gana el Clausura 2024 y permanece con 39 títulos, mientras que Herediano se corona campeón y llega a 30.
Utilizaremos el operador [] para incorporar ambos cambios. Antes, crearemos una copia de df llamada df_csh, de modo que df conserve el escenario original en el que Saprissa gana el torneo en cuestión:
df_csh <- dfPor ahora, df_csh es idéntico a df. Como todavía no hemos modificado sus valores, el Deportivo Saprissa sigue apareciendo con 40 títulos:
df_csh[64, ]# A tibble: 1 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura Saprissa 40
Vamos a reducir ese valor a 39 mediante la indexación:
df_csh[64, 3] <- 39
df_csh[64, ] # A tibble: 1 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura Saprissa 39
Ahora debemos actualizar el total del Club Sport Herediano, que pasará de 29 a 30 títulos:
df_csh[66, ]# A tibble: 1 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura CSH 29
Vamos a aumentar ese valor a 30 mediante la indexación:
df_csh[66, 3] <- 30
df_csh[66, ]# A tibble: 1 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura CSH 30
Ya contamos con los dos data frames que utilizaremos para crear nuestras visualizaciones:
df es el data frame para reproducir el Gráfico A visto al principio, que asume que el Deportivo Saprissa gana el torneo Clausura 2024.
df_csh es el data frame para reproducir el Gráfico B, que asume que el Club Sport Herediano lo gana.
Antes de continuar, utilizaremos subset() para comprobar que los cambios se aplicaron correctamente. Esta función permite seleccionar las observaciones que cumplen una condición específica.
En el escenario representado por df, Saprissa gana el Clausura 2024 y alcanza su título número 40. Comprobemos que los datos reflejen ese resultado:
subset(df, torneo == "2024 Clausura")# A tibble: 3 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura Saprissa 40
2 2024 Clausura LDA 30
3 2024 Clausura CSH 29
En el escenario representado por df_csh, Herediano gana el Clausura 2024 y alcanza su título número 30:
subset(df_csh, torneo == "2024 Clausura")# A tibble: 3 × 3
torneo equipo titulos
<chr> <fct> <dbl>
1 2024 Clausura Saprissa 39
2 2024 Clausura LDA 30
3 2024 Clausura CSH 30
Job done!
Los datos están listos y hemos comprobado que representan correctamente ambos escenarios. Ahora podemos avanzar a la etapa final, y quizá la más interesante, del ejercicio: convertirlos en una visualización.
R es excelente para visualizar datos, y ggplot2 es uno de sus paquetes más utilizados para este propósito.
Como forma parte de tidyverse, ya lo instalamos y cargamos junto con esa colección de paquetes.
Antes de comenzar, conviene entender dos ideas fundamentales de ggplot2:
Un gráfico de ggplot2 se construye por capas. Cada capa incorpora un nuevo componente, como los datos, las formas geométricas o los ajustes visuales.
Las capas se conectan mediante el operador +. No debemos confundirlo con el conocido |>: el operador |> encadena operaciones, mientras que + ensambla los distintos componentes de una visualización.
Manos a la obra. Es hora de reproducir el Gráfico A que vimos al comienzo del post.
Iremos capa por capa.
Comencemos por crear la estructura inicial del gráfico y guardarla como plot9. Las funciones ggplot2::ggplot() y ggplot2::aes() definirán los datos y las variables que utilizaremos; después, iremos añadiendo capas hasta obtener la visualización final:
1plot9 <- df |>
ggplot2::ggplot(
ggplot2::aes(
2 x = factor(torneo, levels = unique(torneo)),
3 y = titulos,
4 group = equipo,
5 color = equipo
)
)
plot9df.

plot9 parece una visualización completamente vacía. Sin embargo, los ejes X y Y ya tienen variables asignadas, al igual que las propiedades de agrupación y color. Esta estructura inicial define la lógica sobre la que construiremos el resto de la visualización.
Siguiente capa: plot8.
plot8 parte de plot9 y agrega una capa que determina el tipo de gráfico que produciremos. En este caso, implementamos la función ggplot2::geom_line(). Al conectar las observaciones con líneas, esta función establece la geometría principal del gráfico:
plot8 <- plot9 +
1 ggplot2::geom_line(linewidth = 2)
plot8linewidth controla el grosor de esas líneas.

plot8 ya revela la estructura de la historia: las líneas permiten seguir cómo cambian los datos a lo largo del tiempo, representado en el eje X. Este tipo de gráfico resulta especialmente útil para mostrar tendencias y trayectorias.
Las tres líneas provienen de la configuración inicial del gráfico: al vincular group y color con la variable equipo, ggplot2 crea y colorea una línea distinta para cada una de sus tres categorías.
Siguiente capa: plot7.
plot7 parte de plot8 y añade una capa que dibuja una línea horizontal mediante la función ggplot2::geom_hline(). Se trata de la línea verde y punteada que observamos en el Gráfico A:
plot7 <- plot8 +
ggplot2::geom_hline(
1 yintercept = 40,
2 color = "#37ae5f",
3 linetype = "dashed"
)
plot7
plot7 coloca la línea horizontal verde y punteada en la posición indicada. Sin embargo, todavía falta la etiqueta de texto que observamos en el Gráfico A.
Siguiente capa: plot6.
plot6 parte de plot7 y añade la etiqueta de texto mediante la función ggplot2::annotate():
plot6 <- plot7 +
ggplot2::annotate(
1 geom = "label",
2 label = " La 40 ",
3 x = 19,
4 y = 40,
5 color = "#216839",
6 fill = "#ebf6ef",
7 fontface = "bold"
)
plot6
La etiqueta " La 40 " ya aparece en plot6. No es el elemento más elegante ni aporta demasiado al diseño, pero nos ofrece una buena oportunidad para practicar cómo añadir anotaciones.
Entre los aspectos que todavía requieren atención, plot6 presenta un defecto importante: las líneas utilizan los colores predeterminados de ggplot2, que comunican menos de lo que podría lograr una paleta elegida cuidadosamente.
Siguiente capa: plot5.
plot5 parte de plot6 e introduce la psicología del color. Para crear plot5, utilizaremos ggplot2::scale_color_manual() y sustituiremos la paleta predeterminada por colores elegidos deliberadamente. La intención no es solo distinguir las líneas, sino también activar asociaciones que ayuden a interpretar el gráfico:
plot5 <- plot6 +
ggplot2::scale_color_manual(
1 values = c("#7b113d", "#cf1f25", "#ffc20f")
)
plot5
En plot5, cada línea adopta el color representativo de su equipo. Para obtener los tonos con precisión, utilizamos imágenes oficiales publicadas en las redes sociales de cada club y extraemos sus códigos mediante un selector de colores en línea como este. Así identificamos, por ejemplo, el tradicional morado vinotinto del Deportivo Saprissa: "#7b113d".
plot5 aprovecha el poder asociativo del color. Sin embargo, los ejes todavía requieren algunos ajustes.
Siguiente capa: plot4.
plot4 parte de plot5 y mejora el eje Y por medio de la función ggplot2::scale_y_continuous(), que permite ajustar su escala con precisión:
plot4 <- plot5 +
ggplot2::scale_y_continuous(
breaks = c(
1 pull(df[64, 3]),
2 pull(df[65, 3]),
3 pull(df[66, 3])
),
4 limits = c(23, 40),
5 position = "right"
)
plot4
plot4 pone en orden el eje Y. En cambio, el eje X sigue siendo ilegible: sus marcas se amontonan unas sobre otras.
Siguiente capa: plot3.
plot3 parte de plot4 y organiza el eje X mediante la función ggplot2::scale_x_discrete(), que permite corregir el evidente problema de saturación. No es necesario mostrar una marca para cada torneo:
plot3 <- plot4 +
ggplot2::scale_x_discrete(
1 breaks = c("2014 Verano", "2024 Clausura"),
2 labels = c("2014 \nVerano", "2024 \nClausura")
)
plot3df; \n inserta un salto de línea entre el año y el nombre del torneo.

plot3 muestra únicamente dos marcas, Verano 2014 y Clausura 2024, para señalar el inicio y el final del periodo analizado.
La visualización ha mejorado considerablemente, pero aún quedan algunos detalles importantes: ajustar los títulos de los ejes y añadir un título y un subtítulo al gráfico.
Siguiente capa: plot2.
plot2 parte de plot3 y ajusta los títulos generales y los títulos de los ejes. Definamos primero estos textos como objetos independientes para mantener el código claro y ordenado:
titulo <- "(A) Si el Deportivo Saprissa gana el Clausura 2024"
subtitulo <- "Conseguiría el título 40, diez más que la LDA"
nota <- 'Fuente: "Programando y visualizando datos en R por primera vez"'Podemos efectuar los cambios con la función ggplot2::labs():
plot2 <- plot3 +
ggplot2::labs(
1 title = titulo,
2 subtitle = subtitulo,
3 caption = nota,
4 x = "",
5 y = ""
)
plot2
plot2 se acerca al resultado que buscamos, pero el fondo gris predeterminado no favorece la visualización. El siguiente paso será sustituirlo.
Siguiente capa: plot1.
plot1 parte de plot2 y modifica el aspecto general del gráfico mediante uno de los temas disponibles en ggplot2, en este caso, ggplot2::theme_classic():
plot1 <- plot2 +
ggplot2::theme_classic()
plot1
plot1 elimina el fondo gris e incorpora otros ajustes estéticos menores que, en conjunto, mejoran la armonía visual del gráfico.
Sin embargo, todavía no podemos considerarlo un producto final: necesitamos aumentar el tamaño del texto.
Siguiente capa: plot.
plot parte de plot1 e incorpora una serie de mejoras: aumenta el tamaño del texto, cambia la fuente y traslada la leyenda al lado izquierdo, entre otros ajustes. Para ello, utilizaremos las funciones ggplot2::theme() y ggplot2::element_text():
plot <- plot1 +
ggplot2::theme(
1 plot.title = ggplot2::element_text(size = 22, face = "bold"),
2 plot.subtitle = ggplot2::element_text(size = 20),
3 plot.caption = ggplot2::element_text(size = 14),
4 axis.text.x = ggplot2::element_text(size = 12, face = "bold"),
5 axis.text.y = ggplot2::element_text(size = 12, face = "bold"),
6 legend.position = "left",
7 legend.title = ggplot2::element_blank(),
8 legend.text = ggplot2::element_text(size = 18),
9 text = ggplot2::element_text(family = "sans")
)
plot
plot completa nuestro recorrido: hemos reproducido el Gráfico A paso a paso.
A lo largo del proceso, vimos cómo construir y refinar una visualización con ggplot2, desde su estructura inicial hasta los últimos ajustes de color, ejes, texto y tipografía.
El proceso anterior permite extraer varias lecciones generales sobre visualización de datos:
Escoger el tipo de visualización correcto según la historia que encierran los datos. La forma de la visualización debe responder a lo que queremos comunicar. Como nuestros datos describen una evolución temporal, utilizamos un gráfico de líneas y situamos el tiempo en el eje X.
Limitar los elementos de la visualización a sólo los estrictamente necesarios. Cada elemento adicional compite por la atención del público. Por eso, simplificamos los ejes y mostramos solo las marcas necesarias. La etiqueta del título 40 no era indispensable, pero cumplió una función didáctica al mostrarnos cómo incorporar anotaciones.
Proporcionar suficiente contexto. Un buen título y un buen subtítulo deben complementarse y permitir que el gráfico se comprenda sin explicaciones externas. Aquí suponemos cierta familiaridad con el fútbol costarricense. Para un público distinto, necesitaríamos añadir más contexto.
Elegir los colores con intención. Los colores no solo distinguen categorías. También pueden activar asociaciones útiles. En este gráfico, cada línea utiliza un color característico del equipo que representa, obtenido a partir de imágenes oficiales.
Diseñar los ejes con moderación. No siempre hacen falta títulos, marcas abundantes ni escalas extensas. En nuestro gráfico, el contexto permite omitir los títulos de los ejes, mientras que unas pocas marcas bien seleccionadas bastan para orientar la lectura.
Ordenar y ubicar la leyenda estratégicamente. Su orden debe guardar relación con la estructura visual del gráfico. Al colocarla a la izquierda, la acercamos a las líneas y evitamos que el eje Y se interponga entre ambas.
Garantizar la legibilidad. Una visualización pierde eficacia si obliga al público a esforzarse para leerla. Ajustar el tamaño y el peso de la tipografía puede transformar significativamente la experiencia de lectura.
Buscar el balance visual. La composición debe distribuir el peso visual de sus elementos. Como el eje Y aparece a la derecha, situamos la leyenda a la izquierda para equilibrar el conjunto.
Someter el gráfico a revisión. Una buena visualización nunca surge en el primer intento. Requiere ajustes, crítica y reelaboración. Incluso después de considerarla terminada, una nueva revisión seguramente revelará oportunidades para mejorarla.
Práctica
A lo largo del ejercicio utilizamos df para reproducir el Gráfico A. Ahora es turno de trabajar con df_csh, el conjunto de datos correspondiente al escenario representado en el Gráfico B.
df_csh para crear un gráfico de líneas que reproduzca el Gráfico B con la mayor fidelidad posible.Alvarado-Mena, E. (2026, 28 de julio). Programando y visualizando datos en R por primera vez. AlvaradoCSS. https://www.alvaradocss.com/posts/programar-visualizar-datos-r/
Alvarado-Mena, Edwin. “Programando y visualizando datos en R por primera vez.” AlvaradoCSS. Publicado originalmente el 10 de marzo de 2025; última modificación el 28 de julio de 2026. https://www.alvaradocss.com/posts/programar-visualizar-datos-r/.
Libros para aprender a razonar y comunicar con datos