Vas a aprender visualización de datos y programación en R con este gráfico futbolero

¿A quién no le gustaría hacer gráficos bonitos y efectivos?

Una pre-introducción a R para personas que nunca han programado.
introductory
data vis
r
code
español
Author

Edwin Alvarado-Mena

Published

March 10, 2025

El Deportivo Saprissa y la Liga Deportiva Alajuelense (LDA) llegaron a la final del torneo Verano 2014 empatados en 29 títulos nacionales. El Deportivo Saprissa ganó esa final y, con ello, su título 30. Bastó una década para que el equilibrio de títulos se rompiera por completo. En enero de 2024 iniciará el torneo Clausura 2024 y el Deportivo Saprissa podría ganar su título 40, diez más que la LDA, estancada en 30 títulos (Gráfico A). Las malas noticias para la LDA no acaban ahí: si el torneo Clausura 2024 lo gana el Club Sport Herediano (CSH), empataría en 30 títulos a la LDA (Gráfico B). Como sea, el Clausura 2024 es un torneo que la LDA no puede perder. Contrario a lo que podría creer cualquier persona no familiarizada con el fútbol de Costa Rica, la debacle de la LDA no ocurrió durante una grave crisis institucional; ocurrió, al contrario, justo en su época de mayor prosperidad económica, con fichajes abusivos e inimaginables (incluyendo un ex director técnico del Real Madrid y numerosos ídolos del Deportivo Saprissa) y una infraestructura deportiva nunca antes vista en el país centroamericano. Spoiler alert: Más de un año después de mi publicación original, LDA perdió tanto el Clausura 2024 como el Apertura 2024, de modo que el Deportivo Saprissa le saca ahora diez títulos de ventaja y el CSH lo empató en 30. Actualizar el código lo dejo como un ejercicio.

Pre-introducción a R

En este ejercicio vamos a presentar todo el código necesario para reproducir el Gráfico A visto arriba.

Para empezar, haremos una pre-introducción a R. Esta pre-introducción es ideal para personas que nunca antes han programado.

¡No hace falta instalar R!

Podés programar en línea. Sólo tenés que crear una cuenta en Posit Cloud para conseguir acceso a RStudio. Podrás entonces realizar este ejercicio de programación aunque no tengás R ni RStudio instalados en tu computadora.

Programar en R consiste en aplicar funciones a objetos:

toupper("hola")
[1] "HOLA"

Arriba, la función toupper() la aplicamos sobre el objeto "hola" y lo convertimos en "HOLA".

La función sqrt() la aplicamos sobre el objeto 4 y obtenemos su raíz cuadrada, 2:

sqrt(4)
[1] 2

Eso es todo: en R, programar consiste en aplicar funciones a objetos.

En los ejemplos anteriores aplicamos funciones a un objeto nada más. 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

Una vez concatenados los objetos, los pasamos a la función que deseemos:

toupper(c("hola", "hello", "olá", "hallo"))
[1] "HOLA"  "HELLO" "OLÁ"   "HALLO"
sqrt(c(1, 100, 4, 55, 10))
[1]  1.000000 10.000000  2.000000  7.416198  3.162278

En estos ejemplos lo que hemos hecho es anidar dos funciones; así, por ejemplo, la función c() quedó anidada dentro de la función toupper().

Las funciones anidadas R las implementa de adentro hacia afuera, de modo que el output de c() se convierte en el input de toupper().

Anidar funciones no es lo mejor si buscamos que nuestro código se lea fácilmente. Con el operador |> podemos conseguir los mismos resultados y a la vez logramos 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) |>
  sqrt()
[1]  1.000000 10.000000  2.000000  7.416198  3.162278

El operador |> clarifica cuál función está tomando como input el output de cuál función. Por supuesto, la utilidad del operador |> es mayor cuantas más funciones estén involucradas en una misma ejecución:

1c("hola", "hello", "olá", "hallo") |>
2  toupper() |>
3  sort()
1
Concatena los objetos.
2
Los convierte en mayúsculas.
3
Los ordena.
[1] "HALLO" "HELLO" "HOLA"  "OLÁ"  
1c(1, 100, 4, 55, 10) |>
2  sqrt() |>
3  sort() |>
4  max()
1
Concatena los objetos.
2
Les saca raíz cuadrada.
3
Los ordena.
4
Escoge el mayor.
[1] 10

Recapitulemos:

  • En R hay funciones y hay objetos.

  • Programar en R consiste en aplicar funciones a objetos.

Lo siguiente es aprender que las funciones tienen argumentos.

R nos permite jugar con los argumentos de las funciones. Modificando sus argumentos podemos adaptar las funciones a nuestras necesidades inmediatas.

Comentarios directo en el código

Todo lo que escribás después de # R lo omitirá. De esa manera podés añadir comentarios al código. Dejar abundancia de comentarios en nuestro código es siempre una buena idea.

Por ejemplo, R dispone de una función para crear secuencias de números:

seq(
  from = 10, # Empieza en este número
  to = 20 # Termina en este 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.

Es curioso que 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.

A falta de indicaciones explícitas, R distribuye los argumentos de acuerdo con el orden asignado por defecto.

La documentación de las funciones es esencial

El orden los argumentos lo podés saber consultando la documentación de las funciones.

Para consultar la documentación de la función seq() basta con ejecutar el shortcut ?seq(). Inmediatamente se desplagará la pestaña Help: en la sección Usage vas a ver el orden asignado por defecto a los argumentos, y en la sección siguiente, Arguments, vas a encontrar una descripción de cada un argumento.

Una de las grandes fortalezas de R es precisamente la excelente calidad de su documentación.

Si a los argumentos los definimos explícitamente, los podemos acomodar en cualquier orden:

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

Las líneas de código demasiado largas no son recomendables porque complican su lectura. Por lo general, es conveniente abordar sólo un argumento por línea:

seq(
  from = 1, 
  to = 21,
  by = 3
)
[1]  1  4  7 10 13 16 19

R permite quebrar las líneas a la altura de los paréntesis y de las comas, y así lo haremos frecuentemente a lo largo de este ejercicio.

Por cierto, el operador : también crea secuencias de números. Lo presentamos desde ya porque lo vamos a necesitar más adelante:

55:60
[1] 55 56 57 58 59 60

R trae consigo un montón de funciones. Ya hemos puesto en práctica unas cuantas: toupper(), sqrt(), c(), sort(), seq().

Una de las fortalezas de R es que podemos instalar paquetes con más funciones. La oferta de paquetes que existe es realmente extraordinaria. Hay paquetes para programar casi cualquier cosa.

Para este ejercicio vamos a instalar el paquete tidyverse, el cual es, en realidad, una colección de paquetes. Al instalar tidyverse estamos instalando varios paquetes al mismo tiempo, entre otros:

  • dplyr: paquete especializado en manipulación de datos.

  • ggplot2: paquete especializado en visualización de datos.

El primer paso es instalar tidyverse usando la función install.packages():

install.packages("tidyverse")

El segundo paso es cargarlo usando la función library():

library(tidyverse)

Ambos pasos (instalar y cargar el paquete) son indispensables, no importa el paquete en cuestión.

Aunque hayamos instalado un paquete en nuestra computadora previamente, si no lo hemos cargado en la sesión de R actual no será posible utilizar sus funciones.

¡Listo! Esta pre-introducción a R es suficiente para que cualquier persona sin experiencia previa en programación pueda encarar con éxito las próximas secciones.

Manipulación de datos

Entramos en materia, ahora sí. Primero vamos a producir los datos que necesitamos para este ejercicio.

Los datos corresponden a los torneos disputados en la liga profesional de fútbol masculino de Costa Rica, desde el torneo Invierno 2013 hasta el torneo Clausura 2024:

data <- 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 gana
  "2018 Clausura", 34, 29, 26, # Saprissa gana 34
  "2018 Apertura", 34, 29, 27, # CSH gana 27
  "2019 Clausura", 34, 29, 27, # Ninguno 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 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 produce los datos que vamos a visualizar en la próxima sección.

El origen de los datos

Lo usual es que tengás que cargar los datos (típicamente nos los dan almacenados, digamos, en un spreadsheet como las conocidas hojas de cálculo de Microsoft Excel). Por mera conveniencia, para este ejercicio los datos los produje manualmente, valiéndome de la función tribble() para tabularlos. Los datos los obtuve de Wikipedia.

No vamos a profundizar en cómo opera la función tribble(). Lo importante es notar que el operador <- toma el output de dicha función (los datos) y lo asigna a un nombre: data.

A partir del instante en que ejecutamos ese bloque de código el objeto data ha sido creado en la memoria de nuestra computadora. En adelante, podremos manipular los datos con tan sólo llamarlos por su nombre: data.

Por ejemplo, si queremos saber cuántas filas tiene data, le aplicamos la función nrow():

nrow(data)
[1] 22

Si queremos imprimirlos, 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

La función print() imprime apenas diez filas. Esta función tiene un argumento, n, que podemos modificar para que imprima un número específico de filas:

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

Tal como acabamos de observar, la función nrow() señala cuántas filas tiene el objeto data y la función print() imprime unas cuantas filas (por defecto, n = 10). O sea, bien podríamos combinar ambas funciones para lograr que R imprima todas las filas:

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 imprimimos justo arriba:

  • data recoge cuántos títulos lleva acumulados cada uno de los tres equipos seleccionados (Deportivo Saprissa, Liga Deportiva Alajuelense y Club Sport Herediano) al finalizar el torneo respectivo; cada fila es un torneo distinto.

  • data incluye el torneo Clausura 2024 (no habrá iniciado al momento de publicarse este ejercicio originalmente) y asume que el Deportivo Saprissa lo ganará; este es el escenario hipotético al que se refiere el Gráfico A visto al principio, y recordemos que nuestro objetivo es reproducir ese Gráfico A.

  • data esconde una historia: la historia de cuántos títulos de ventaja sacaría el Deportivo Saprissa sobre la Liga Deportiva Alajuelense si ganase el Clausura 2024 (acaba de ganar el Apertura 2023) y cómo la ventaja del primero sobre el segundo se abrió escandalosamente en cuestión de una década; esta es la historia que el Gráfico A comunica.

Por cierto, si necesitamos contar las columnas de data en lugar de las filas, la función ncol() hará el trabajo:

ncol(data)
[1] 4

La función dim() devuelve el número de filas y de columnas, en ese orden:

dim(data)
[1] 22  4

Podemos salvar el resultado anterior con el ya conocido operador <-, por ejemplo, asignando ese objeto al nombre dimensiones_originales.

Recordemos que la asignación hecha mediante <- crea el objeto en la memoria de nuestra computadora para que lo podamos usar repetidamente, pero no lo imprime:

## Lo asignamos
dimensiones_originales <- dim(data)

Hay dos opciones para imprimir el objeto:

## Lo imprimimos, opción A
dimensiones_originales
[1] 22  4
## Lo imprimimos, opción B
print(dimensiones_originales)
[1] 22  4

Asignar un objeto a un nombre y al mismo tiempo imprimirlo es posible si encapsulamos todo el código entre paréntesis:

## Lo asignamos y lo imprimimos
(dimensiones_originales <- dim(data))
[1] 22  4

Lista la producción de los datos. Ahora vamos a modificarlos un poco.

Sobre todo, necesitamos llevar a cabo un cambio importante respecto a la estructura misma de los datos.

data actualmente tiene una columna Saprissa, una columna LDA y una columna CSH.

Vamos a imprimir unas cuantas filas para recordar cómo se ve. Las funciones head() y tail() imprimen las primeras y las últimas filas, respectivamente:

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

A efectos de visualizar estos datos, en lugar de tener esas tres columnas así, separadas por equipos, necesitamos colapsarlas en una única columna equipo, de la cual Saprissa, LDA y CSH sean sus tres posibles valores.

Dicha transformación la conseguiremos mediante las funciones pivot_longer() y mutate(), ambas del paquete tidyverse (de los paquetes dplyr y tidyr, en realidad, dos de los varios paquetes que están incorporados en tidyverse).

Vamos a analizar la transformación paso por paso:

1df <- data |>
2  pivot_longer(
3    cols = c("Saprissa", "LDA", "CSH"),
4    names_to = "equipo",
5    values_to = "titulos"
  ) |>
  mutate( 
6    equipo = factor(
      equipo, 
      levels = c("Saprissa", "LDA", "CSH")
    )
  )
1
Asigna el objeto (el objeto que resulta de todo este bloque de código) al nombre df.
2
Lo convierte de ancho a largo (es decir, colapsamos columnas).
3
Indica las columnas que dejarán de serlo (es decir, las que vamos a colapsar en una única columna).
4
Indica el nombre de la (nueva) columna a la que irán los nombres de las columnas anteriores.
5
Indica el nombre de la (nueva) columna a la que irán los valores de las columnas anteriores.
6
Convierte en factor la (nueva) columna equipo, un paso necesario para especificar el orden de los equipos (Saprissa > LDA > CSH) y del que no hará falta que profundicemos mucho más.

Lo que acabamos de hacer es transformar los datos y almacenarlos en un objeto distinto: df.

data y df tienen ahora estructuras distintas.

Los datos originales hay que conservarlos

Es siempre aconsejable que conservés una copia intacta de los datos tal cual eran originalmente, de ahí que el resultado de la transformación a la que sometí a data lo asigné al nombre df.

Los data sets data y df son diferentes:

  • data son los datos originales, que voy a conservar por si más adelante necesito verlos y recordar cómo eran.

  • df son los datos que estoy manipulando y que seguiré manipulando.

Los datos en df tienen esta forma (mucha atención a las columnas equipo y titulos, columnas que no existen en data):

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

Esto es una transformación. La estructura de los datos ha cambiado: df tiene menos columnas y muchas más filas que data.

Las dimensiones originales eran estas:

dimensiones_originales # filas y columnas, en ese orden
[1] 22  4

Las nuevas dimensiones son estas:

dim(df) # filas y columnas, en ese orden
[1] 66  3

En otras palabras, la transformación consistió en pasar de un data set ancho, data (22 filas y 4 columnas), a uno largo, df (66 filas y 3 columnas).

Además de una nueva columna equipo en lugar de las tres columnas Saprissa, LDA, CSH, ahora existe una columna que indica el número de titulos que cada equipo acumuló al finalizar cada torneo.

Tidy data

Por razones que no podré elaborar aquí, la estructura de los datos que hemos consolidado hasta este punto es la que más nos conviene a efectos de visualizarlos. La explicación técnica y pormenorizada la podés leer aquí.

df asume que Saprissa ganará el título Clausura 2024. Podemos explorar esa región del data set por medio del operador [], que nos permite hacer selecciones de filas y columnas con tan sólo especificar sus índices.

Cuando usamos el operador [], tal como vamos a mirar en el siguiente ejemplo, antes de la coma establecemos los índices de las filas que nos interesa seleccionar; 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 no indicamos nada después de la coma, R asumirá que queremos 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

Si no indicamos nada antes de la coma, R asumirá que queremos todas las filas:

## Todas las filas, columna 1
## Pero R no me las va a imprimir todas
## Me va a imprimir sólo 10 filas
## Porque son un buen poco de filas     D:
## Pero creo que se entiende el punto    :D
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

Si especificamos sólo una fila y sólo una columna, esto es básicamente como suministrarle a R las coordenadas exactas para seleccionar una casilla específica:

df[10, 1]
# A tibble: 1 × 1
  torneo     
  <chr>      
1 2015 Verano

La casilla del título 40 del Deportivo Saprissa (en nuestro caso hipótetico, la que asumimos que ganará en el Clausura 2024) se extrae así:

df[64, 3]
# A tibble: 1 × 1
  titulos
    <dbl>
1      40

La indexación habilita manipulaciones muy útiles. Podemos, por ejemplo, introducir cambios.

Digamos que el torneo Clausura 2024 no lo gana el Deportivo Saprissa (quedaría entonces en 39 títulos) sino el Club Sport Herediano (pasaría a 30 títulos).

El operador [] nos da una mano para llevar a cabo los dos cambios anteriores. Para empezar, vamos a crear un data set diferente, df_heredia, de manera tal que df lo mantendremos como el data set en el que asumimos que el Deportivo Saprissa ganará el torneo Clausura 2024:

df_heredia <- df

En este instante, df_heredia es una copia de df, así que no hay ninguna sorpresa si el Deportivo Saprissa sigue apareciendo con 40 títulos:

df_heredia[64, ]
# A tibble: 1 × 3
  torneo        equipo   titulos
  <chr>         <fct>      <dbl>
1 2024 Clausura Saprissa      40

Vamos a bajarlo a 39 títulos usando la indexación:

df_heredia[64, 3] <- 39
df_heredia[64, ] 
# A tibble: 1 × 3
  torneo        equipo   titulos
  <chr>         <fct>      <dbl>
1 2024 Clausura Saprissa      39

Listo. Lo siguiente es sumarle un título más al Club Sport Herediano, que actualmente aparece con 29:

df_heredia[66, ]
# A tibble: 1 × 3
  torneo        equipo titulos
  <chr>         <fct>    <dbl>
1 2024 Clausura CSH         29

Vamos a subirlo a 30 títulos usando la indexación:

df_heredia[66, 3] <- 30
df_heredia[66, ]
# A tibble: 1 × 3
  torneo        equipo titulos
  <chr>         <fct>    <dbl>
1 2024 Clausura CSH         30

Ahora tenemos dos data sets aptos para nuestro ejercicio de visualización de datos:

  • df es el data set para reproducir el Gráfico A visto al principio, el que asume que el Deportivo Saprissa obtiene el torneo Clausura 2024.

  • df_heredia es el data set para reproducir el Gráfico B visto al principio, el que asume que el Club Sport Herediano obtiene el torneo Clausura 2024.

La función subset() nos ayudará a realizar unas verificaciones finales. Básicamente, esta función sirve para filtrar los datos.

df asume que el torneo Clausura 2024 será el título 40 del Deportivo Saprissa:

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

df_heredia asume que el torneo Clausura 2024 será el título 30 del Club Sport Herediano:

subset(df_heredia, 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!

Con estos data sets debidamente producidos podemos pasar a la fase final y más interesante de este ejercicio: la visualización de datos.

Visualización de datos

R es excelente para visualizar datos. El paquete más popular para visualización de datos en R se llama ggplot2. Lo instalamos y cargamos al instalar y cargar tidyverse.

Hay dos características de ggplot2 que debemos conocer desde el principio:

  • Las visualizaciones las vamos a ir construyendo por capas. Pronto veremos qué significa construir visualizaciones “por capas”.

  • El operador + nos permite ir agregando las capas. Los operadores |> (aquí lo hemos utilizado antes) y + hacen básicamente lo mismo, pero ggplot2 sólo acepta +.

Manos a la obra. Es hora de reproducir el Gráfico A que vimos al principio del post.

Vamos a crear un gráfico y se lo vamos a asignar al nombre plot9. Utilizando las funciones ggplot() y aes(), producimos la primera capa a la que le iremos agregando otras capas hasta nuestro producto final:

1plot9 <- df |>
  ggplot(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 que corresponde al eje X (requirió una transformación que no es necesario profundizar).
3
Indica la variable que corresponde al eje Y.
4
Indica la variable que más adelante usaremos para agrupar.
5
Indica la variable que más adelante usaremos para colorear.

plot9 parece una visualización completamente vacía, pero si ponemos atención notaremos que los ejes X y Y ya están ocupados. De hecho, esta capa define todas las variables que van a dar sentido a la visualización en su conjunto.

Siguiente capa: plot8.

plot8 parte de plot9 y le agrega una capa en la que determinamos el tipo de gráfico que vamos a producir. En este caso, implementamos la función geom_line() pues nuestro objetivo es crear un gráfico lineal:

plot8 <- plot9 +
1  geom_line(linewidth = 2)

plot8
1
Crea un gráfico lineal (el argumento linewidth determina el grosor de la línea).

plot8 ya no se ve vacío. Es un gráfico lineal, exactamente el tipo de gráfico que resulta adecuado para comunicar datos que evolucionan en el tiempo (eje X).

Son tres líneas porque así lo habíamos establecido en los argumentos group y color de la capa anterior: ambos los asociamos con la variable equipo, que abarca tres categorías.

Siguiente capa: plot7.

plot7 parte de plot8 y le añade una capa que dibuja una línea horizontal mediante la función geom_hline(). Esta es la misma línea horizontal, verde y punteada que observamos en el Gráfico A:

plot7 <- plot8 +
  geom_hline(
1    yintercept = 40,
2    color = "#37ae5f",
3    linetype = "dashed"
  ) 

plot7
1
Indica dónde se ubica la línea horizontal respecto al eje Y.
2
Indica el color de la línea.
3
Indica el tipo de línea.

plot7 instala en su lugar la línea horizontal, verde y punteada. Falta, sin embargo, la etiqueta de texto que también observamos en el Gráfico A.

Siguiente capa: plot6.

plot6 parte de plot7 y le suma la etiqueta de texto, para lo cual implementamos la función annotate():

plot6 <- plot7 +
  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
Indica el tipo de etiqueta.
2
Indica el texto.
3
Indica dónde se ubica la etiqueta respecto al eje X.
4
Indica dónde se ubica la etiqueta respecto al eje Y.
5
Indica el color de los bordes de la etiqueta.
6
Indica el color del relleno de la etiqueta.
7
Indica el énfasis del texto (en negrita).

plot6 cumple la misión de instalar en su lugar la etiqueta " La 40 ". Pero aún nos queda mucho por hacer y mejorar.

Entre otras cosas que requieren atención, plot6 tiene un defecto importante: los colores de las líneas son los colores genéricos de ggplot2, colores que no transmiten todo lo que podrían comunicar colores mejor escogidos.

Siguiente capa: plot5.

plot5 parte de plot6 e introduce la importancia de la psicología del color. Con la función scale_color_manual() modificaremos los colores de modo muy intencional para propiciar ciertas conexiones mentales:

plot5 <- plot6 +
  scale_color_manual(
1    values = c("#7b113d", "#cf1f25", "#ffc20f")
  ) 

plot5
1
Establece los colores icónicos de cada equipo.

plot5 consigue que las líneas sean de un color u otro según cuál sea el equipo. Para elegir los colores con exactitud, en este sitio web cargamos una imagen oficial tomada de las redes sociales de cada equipo y empleamos el extractor para identificar los códigos de aquellos colores que definen su identidad cromática. En el caso del Deportivo Saprissa, por ejemplo, su tradicional morado vinotinto tiene el código de color "#7b113d".

plot5 aprovecha al máximo la psicología del color. Ahora bien, los ejes son todavía muy mejorables.

Siguiente capa: plot4.

plot4 parte de plot5 y lo dedicamos a arreglar el eje Y por medio de la función scale_y_continuous(), que nos permite efectuar precisas modificaciones:

plot4 <- plot5 +
  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
Establece la marca (es decir, el número de títulos) que acumula el Deportivo Saprissa en el último torneo de la serie.
2
Establece la marca (es decir, el número de títulos) que acumula la Liga Deportiva Alajuelense en el último torneo de la serie.
3
Establece la marca (es decir, el número de títulos) que acumula el Club Sport Herediano en el último torneo de la serie.
4
Determina los límites del eje Y.
5
Determina la posición del eje Y, que trasladamos a la derecha.

plot4 ordena el eje Y, en efecto, pero el eje X continúa siendo un desastre. Es más, el eje X ni siquiera se puede leer pues las marcas están todas superpuestas.

Siguiente capa: plot3.

plot3 parte de plot2 y ordena el eje X mediante la función scale_x_discrete(), que nos permite atacar el evidente problema de saturación (ni siquiera hace falta dejar una marca para cada torneo):

plot3 <- plot4 +
  scale_x_discrete(
1    breaks = c("2014 Verano", "2024 Clausura"),
2    labels = c("2014 \nVerano", "2024 \nClausura")
  )

plot3
1
Indica las marcas del eje X, que limitamos a dos únicamente.
2
Modifica los nombres de las marcas del eje X, que no son exactamente iguales a como aparecen en el data set df.

plot3 limita las marcas a aquellas que corresponden a los torneos Verano 2014 y Clausura 2024, ya que esos son los dos torneos que fijan la década en la cual la Liga Deportiva Alajuelense experimentó su debable deportiva.

Es un notable paso hacia adelante, pero los títulos de los ejes son aún mejorables; además, al gráfico le falta título y subtítulo.

Siguiente capa: plot2.

plot2 parte de plot3 y arregla los títulos del gráfico, tanto los generales como los de los ejes. Para que no se nos haga demasiado bulto en el código, los textos definámoslos por adelantado:

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: "Vas a aprender visualización de datos y programación en R con este gráfico futbolero"'

Ahora sí, podemos efectuar los cambios gracias a la función labs():

plot2 <- plot3 + 
  labs(
1    title = titulo,
2    subtitle = subtitulo,
3    caption = nota,
4    x = "",
5    y = ""
  ) 

plot2
1
Establece el título.
2
Establece el subtítulo.
3
Establece la nota al pie.
4
Establece el título del eje X (lo dejamos vacío).
5
Establece el título del eje Y (lo dejamos vacío).

plot2 se empieza a mirar bastante aceptable. Ahora bien, el fondo gris del gráfico es genérico y muy feo. Lo vamos a cambiar.

Siguiente capa: plot1.

plot1 parte de plot2 y retoca el aspecto general del gráfico; lo hacemos escogiendo alguna estética de las que ggplot2 dispone, como theme_classic():

plot1 <- plot2 +
  theme_classic()

plot1

plot1 elimina el fondo gris, entre otros retoques estéticos menores pero que todos juntos armonizan muy gratamente el aspecto general del gráfico.

No podríamos hablar de un producto final, sin embargo, mientras no incrementemos el tamaño de la letra.

Siguiente capa: plot.

plot parte de plot1 y desarrolla una amplia gama de mejoras: vamos a hacer más grande el texto, cambiar la fuente, llevar la tabla de las leyendas al lado izquierdo, entre otros arreglos que la función theme() en conjunto con la función element_text() hacen realidad:

plot <- plot1 +
  theme(
1    plot.title = element_text(size = 22, face = "bold"),
2    plot.subtitle = element_text(size = 20),
3    plot.caption = element_text(size = 14),
4    axis.text.x = element_text(size = 12, face = "bold"),
5    axis.text.y = element_text(size = 12, face = "bold"),
6    legend.position = "left",
7    legend.title = element_blank(),
8    legend.text = element_text(size = 18),
9    text = element_text(family = "sans")
  ) 

plot
1
Retoca el tamaño y el énfasis del título.
2
Retoca el tamaño del subtítulo.
3
Retoca el tamaño de la nota al pie.
4
Retoca el tamaño y el énfasis de las marcas del eje X.
5
Retoca el tamaño y el énfasis de las marcas del eje Y.
6
Traslada la tabla de leyendas a la izquierda.
7
Desaparece el título de la tabla de leyendas.
8
Retoca el tamaño del texto de la tabla de leyendas.
9
Establece la fuente para todo el gráfico.

plot es el producto final: una copia exacta del Gráfico A.

Hemos ilustrado así el proceso completo de cómo crear una visualización efectiva utilizando el paquete ggplot2.

Buenas prácticas

Algunas buenas prácticas de visualización de datos que es importante resaltar:

  • Escoger el tipo de visualización correcto según la historia que encierran los datos. Nuestros datos refieren a la evolución de un evento (la obtención de títulos) a lo largo del tiempo. Por ende, escogimos un gráfico lineal que desarrolla la variable temporal a lo largo del eje X.
  • Limitar los elementos de la visualización a sólo los estrictamente necesarios. Si al gráfico le metíamos mucha cosa, lo arruinábamos. Particularmente los ejes los diseñamos de modo muy minimalista, ambos con unas poquitas marcas apenas. La etiqueta del título 40 probablemente no era necesaria. La incluí sólo para ilustrar que añadir etiquetas es posible.
  • Incluir títulos y subtítulos que permitan a la visualización explicarse por sí sola. El gráfico tiene un título y un subtítulo que se refuerzan mutuamente. Damos por un hecho que el público meta está familiarizado con el fútbol de Costa Rica. Si no fuera el caso, ajustes adicionales serían necesarios.
  • Explotar la psicología del color siempre que sea posible. Cada equipo es representado por uno de sus colores icónicos. Son sus colores exactos. Lo podemos garantizar porque tuvimos el cuidado de buscar los códigos de color respectivos.
  • Evitar la sobrecarga de texto y la desproporción en los ejes X y Y. El gráfico ni siquiera tiene títulos para los ejes. Confiamos en que el título, el subtítulo y, sobre todo, las marcas en cada eje bastan para saber de qué se trata cada uno. Los límites de los ejes no alargan ni achican la visualización.
  • Procurar que el orden de la tabla de leyendas sea consistente con lo que la visualización transmite. En este caso, la tabla de leyendas está ordenada de un modo consistente con el orden mismo que refleja el gráfico; la ubicamos a la izquierda para que dicha conexión sea aún más cercana a las líneas de cada equipo (el eje Y no queda en medio).
  • Ajustar el texto para que las personas puedan leerlo sin esfuerzo. Es frecuente topar con visualizaciones ilegibles debido a que el tamaño de la letra es inadecuado. Un ajuste así de elemental puede hacer toda la diferencia.
  • Preservar el balance de la visualización. Los distintos elementos hay que repartirlos a lo largo y ancho del gráfico de la forma más balanceada posible: si el eje Y cae a la derecha, como en este caso, la tabla de leyendas la coloco a la izquierda para compensar.
  • Buscar la crítica. Las visualizaciones efectivas requieren elaboración y reelaboración, y extrema atención al detalle. Si a este mismo gráfico lo volvemos a revisar la próxima semana con plena seguridad le encontraremos numerosas oportunidades de mejora.

Práctica

Hemos utilizado el data set df para reproducir el Gráfico A visto al principio del post. También habíamos creado el data set df_heredia, con base en el cual es posible reproducir el Gráfico B.

  • Crear un gráfico lineal con base en df_heredia, tan parecido como sea posible al Gráfico B.

Cite my blog posts as follows:
Alvarado-Mena, Edwin. (Year, Month Date). Title. AlvaradoCSS. URL.

Any strong opinions about this post?
Please let me know! I take your feedback very seriously.


Photo by Click and Boo on Unsplash