Programando y visualizando datos en R por primera vez

Paso a paso hasta crear un gráfico con R y ggplot2

Una pre-introducción a R para personas que nunca han programado.
Español
R
Programación
Manipulación de datos
Visualización de datos
Autor/a

Edwin Alvarado-Mena

Fecha de publicación

10 de marzo de 2025

Fecha de modificación

28 de julio de 2026

Photo by Click and Boo on Unsplash

Dos gráficos de líneas muestran la evolución de los títulos de Saprissa, LDA y Herediano entre 2014 y 2024 bajo dos posibles resultados del Clausura 2024.

El Deportivo Saprissa y la Liga Deportiva Alajuelense (LDA) llegaron a la final del torneo Verano 2014 empatados con 29 títulos nacionales. Saprissa ganó aquella final y alcanzó su título 30. Diez años después, la relación entre los principales clubes del país había cambiado considerablemente.
Este ejercicio planteaba dos posibles desenlaces antes del torneo Clausura 2024: Saprissa podía alcanzar su título 40 y establecer una diferencia de diez campeonatos sobre la LDA, que sumaba 30 (Gráfico A), o el Club Sport Herediano (CSH) podía ganar el torneo y alcanzar también los 30 títulos de la LDA (Gráfico B). En ambos escenarios, el torneo tenía consecuencias importantes para el balance histórico entre los clubes.
Este cambio resulta particularmente interesante porque no ocurrió durante una etapa de debilidad institucional o económica de la LDA. Por el contrario, coincidió con un periodo de importantes recursos, contrataciones destacadas y una infraestructura deportiva excepcional dentro del contexto costarricense. La diferencia entre esas capacidades institucionales y los resultados obtenidos constituye, precisamente, uno de los aspectos más llamativos del caso.

Spoiler alert: Al cierre de 2024, Saprissa tenía 40 títulos, mientras que LDA y CSH registraban 30 cada uno. Actualizar el código queda como ejercicio.

Pre-introducción a R para personas que nunca han programado

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:

1c("hola", "hello", "olá", "hallo") |>
2  toupper() |>
3  sort()
1
c() concatena los objetos.
2
toupper() los convierte en mayúsculas.
3
sort() los ordena.
[1] "HALLO" "HELLO" "HOLA"  "OLÁ"  
1c(1, -100, 4, -55, 10) |>
2  abs() |>
3  sort() |>
4  max()
1
c() concatena los objetos.
2
abs() los convierte en valores absolutos.
3
sort() los ordena.
4
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.

Manipulación de datos

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")
    )
  )
1
Asigna el objeto que resulta de todo este bloque de código al nombre df, para guardarlo en la memoria de la sesión.
2
Reorganiza los datos: los lleva de un formato ancho, con una columna por equipo, a un formato largo de una única columna para los tres equipos (es decir, colapsa columnas).
3
Selecciona las tres columnas que se combinarán: Saprissa, LDA y CSH.
4
Crea la columna equipo, que contendrá los nombres de las columnas seleccionadas.
5
Crea la columna titulos, que contendrá los valores almacenados en esas columnas.
6
Convierte en factor la nueva columna 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 <- df

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

Visualización de datos

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

plot9
1
Indica los datos a visualizar, o sea, df.
2
Indica la variable asignada al eje X; esta variable requirió una transformación adicional en la que no necesitamos profundizar.
3
Indica la variable asignada al eje Y.
4
Indica la variable que utilizaremos más adelante para agrupar los datos.
5
Indica la variable que utilizaremos más adelante para asignar los colores

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)

plot8
1
Añade una capa que conecta las observaciones mediante líneas. El argumento linewidth 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
1
Indica la posición de la línea horizontal en el eje Y.
2
Define el color de la línea.
3
Define el tipo de línea.

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
1
Especifica que la anotación será una etiqueta.
2
Establece el texto que aparecerá en la etiqueta.
3
Define dónde se ubica la etiqueta respecto al eje X.
4
Define dónde se ubica la etiqueta respecto al eje Y.
5
Establece el color del texto y del contorno.
6
Establece el color de fondo de la etiqueta.
7
Especifica el énfasis del texto (negrita).

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
1
Establece los colores icónicos de cada equipo.

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
1
Extrae el total final de títulos del Deportivo Saprissa y lo utiliza como una marca del eje Y.
2
Hace lo mismo con el total final de la Liga Deportiva Alajuelense.
3
Repite la operación con el total final del Club Sport Herediano.
4
Limita la escala del eje Y al intervalo comprendido entre 23 y 40 títulos.
5
Coloca el eje Y en el lado derecho de la visualización.

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

plot3
1
Define las marcas que aparecerán en el eje X, que limitamos a dos.
2
Modifica las etiquetas de esas marcas para presentarlas de manera distinta a como aparecen en el conjunto de datos df; \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
1
Establece el título del gráfico.
2
Establece el subtítulo.
3
Añade una nota al pie.
4
Deja vacío el título del eje X.
5
EDeja vacío el título del eje Y.

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
1
Ajusta el tamaño y el énfasis del título.
2
Ajusta el tamaño del subtítulo.
3
Ajusta el tamaño de la nota al pie.
4
Ajusta el tamaño y el énfasis de las marcas del eje X.
5
Ajusta el tamaño y el énfasis de las marcas del eje Y.
6
Traslada la leyenda al lado izquierdo.
7
Oculta el título de la leyenda.
8
Ajusta el tamaño del texto de la leyenda.
9
Establece la fuente tipográfica de todo el gráfico.

Line chart of Costa Rican football titles from Verano 2014 to Clausura 2024. Saprissa rises from 29 to a projected 40 titles, while LDA remains at 30 and Herediano rises from 23 to 29.

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.

Buenas prácticas

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.

  • Utilizar df_csh para crear un gráfico de líneas que reproduzca el Gráfico B con la mayor fidelidad posible.

Compartir este post
LinkedIn Bluesky X WhatsApp

Referencia sugerida

APA

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/

Chicago

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


Bonus track

Libros para aprender a razonar y comunicar con datos