Las redes relacionales son una manera de visualizar información que resulta muy útil para datos cualitativos y cuantitativos.

Como su nombre lo indica, este tipo de redes son utilizadas para mostrar relaciones entre datos, generalmente nominales (nombres, categorías, etiquetas). Por ejemplo, la afinidad entre los integrantes de un equipo de trabajo, los principales clientes de diferentes centros de distribución, dependencias entre procesos y muchas otras cosas más.

Formalmente podríamos decir que este tipo de visualizaciones es un grafo, cuyas características son descritas usando como marco de referencia la teoría de grafos:

Esta teoría puede resultar un poco compleja, pero para fines prácticos, un grafo es una estructura que describe un conjunto de vértices, nodos o puntos y que están unidos por aristas, arcos o líneas.

Si visualizamos esta estructura, obtendremos algo como lo siguiente:

Para esta entrada, usaremos como ejemplo datos de la serie de juegos Pokémon, así que para empezar, hablaremos de de qué trata este juego y después veremos cómo crear, paso a paso, una red relacional usando los paquetes ggraph e igraph de R.

Una introducción muy general a Pokémon

Pokémon es una franquicia de videojuegos portátiles que tuvo su origen en 1996, en la consola Game Boy de Nintendo.

Este es un juego de rol (RPG) en el que los jugadores asumen el papel de un entrenador de los titulares pokemon (pocket monsters), criaturas muy diversas con habilidades especiales, cuya meta es capturar y entrenar a estos simpáticos bichos para hacerlos luchar unos contra otros. O hacerlos concursar en certámenes de belleza, utilizarlos como medio de transporte, criarlos como mascotas y muchas cosas más.

Veinte años de videojuegos han dado mucho material, que ha dado lugar a juguetes, películas, música, ropa, libros y todo lo que sea posible imaginar decorado con los personajes de Pokémon.

Al momento de escribir esto, existen 802 especies diferentes de pokemon y cada una de ellas tiene un tipo que determina sus estadísticas de ataque y defensa así como las habilidades que puede aprender.

Por ejemplo, el famosísimo Pikachu es, en realidad, un miembro de toda una especie de pokemon de tipo eléctrico, mientras que Bidoof, un mítico pokemon con características cuasi-divinas, es de tipo normal.

Estos tipos de pokemon determinan fortalezas y debilidades de cada especie con respecto a otra en un complejo sistema, similar al juego “Piedra, papel y tijera”.

Por ejemplo, un pokemon de tipo agua es fuerte contra uno de tipo fuego estos, a su vez, son fuertes contra los de tipo planta, y este último tipo de pokemon es fuerte contra los de tipo agua. Más precisamente, son fuertes o débiles ante ataques de un tipo particular, pero para no complicarnos, pensemos en términos de especie y tipo.

Lo anterior equivale a decir que hay una relación fortaleza - debilidad entre los tipos de pokemon Esta sería fácil de esquematizar si sólo tuviéramos los cuatro tipos arriba listados, pero no es así. Actualmente, existen dieciocho tipos diferentes de pokemon y cada uno puede ser fuerte o débil a más de un tipo. En algunos casos, la fortaleza es recíproca y hay tipos que son fuertes hacía sí mismos.

Lo que haremos es representar estas relaciones complejas de una manera intuitiva de comprender: Una red relacional.

Y para crearla, preparemos nuestro espacio de trabajo.

Paquetes necesarios

Utilizaremos los siguientes paquetes para este análisis.

Como siempre, si no cuentas con alguno de estos paquetes puedes usar la función install.packages() para instalarlos.

Cargamos los paquetes a nuestro espacio de trabajo con library().

Descarga de datos

Usaremos la copia de un conjunto de datos publicado originalmente en Kaggle. Este conjunto contiene datos de 800 especies de pokemon distintas con varias características para cada uno de ellos.

The Complete Pokémon Dataset:

Descargaremos este conjunto de datos desde Github y obtenemos como resultado el archivo “pokemon.csv”.

Lectura de datos

Como tenemos un archivo con extensión .csv, usamos la función read_csv.

Veamos nuestro resultado.

## # A tibble: 801 x 41
##    abilities against_bug against_dark against_dragon against_electric
##    <chr>           <dbl>        <dbl>          <dbl>            <dbl>
##  1 ['Overgr~        1               1              1              0.5
##  2 ['Overgr~        1               1              1              0.5
##  3 ['Overgr~        1               1              1              0.5
##  4 ['Blaze'~        0.5             1              1              1  
##  5 ['Blaze'~        0.5             1              1              1  
##  6 ['Blaze'~        0.25            1              1              2  
##  7 ['Torren~        1               1              1              2  
##  8 ['Torren~        1               1              1              2  
##  9 ['Torren~        1               1              1              2  
## 10 ['Shield~        1               1              1              1  
## # ... with 791 more rows, and 36 more variables: against_fairy <dbl>,
## #   against_fight <dbl>, against_fire <dbl>, against_flying <dbl>,
## #   against_ghost <dbl>, against_grass <dbl>, against_ground <dbl>,
## #   against_ice <dbl>, against_normal <dbl>, against_poison <dbl>,
## #   against_psychic <dbl>, against_rock <dbl>, against_steel <dbl>,
## #   against_water <dbl>, attack <dbl>, base_egg_steps <dbl>,
## #   base_happiness <dbl>, base_total <dbl>, capture_rate <chr>,
## #   classfication <chr>, defense <dbl>, experience_growth <dbl>,
## #   height_m <dbl>, hp <dbl>, japanese_name <chr>, name <chr>,
## #   percentage_male <dbl>, pokedex_number <dbl>, sp_attack <dbl>,
## #   sp_defense <dbl>, speed <dbl>, type1 <chr>, type2 <chr>,
## #   weight_kg <dbl>, generation <dbl>, is_legendary <dbl>

Procesamiento de datos

Ahora sí, empezamos con lo interesante, procesar los datos.

¿Por dónde empezamos?

Recordemos que nuestra intención es mostrar las relaciones entre tipos de pokemon, así nuestro procesamiento debe facilitar esto.

Para construir una red relacional necesitamos una tabla de datos como la siguiente:

Desde Hacia Característica1 Característica2
Uno Dos Fuerza Non
Dos Uno Debilidad Par
Uno Tres Fuerza Non
Dos Tres Debilidad Par

Cada renglón describe una relación y necesitamos al menos dos columnas, una con el origen de la relación (desde donde surge) y otra con el destino de ella (hacia donde termina).

Podemos tener múltiples columnas adicionales que describan las características de la relación en cada renglón.

Considerando lo anterior, nuestra meta mínima es obtener una tabla como la siguiente.

Tipo (origen) Tipo (destino) Relación
Agua Fuego Fuerza
Fuego Agua Debilidad
Planta Agua Fuerza
Planta Fuego Debilidad

Nuestro objeto pokemon tiene dos columnas con información de tipo de pokemon: type1 y type2. Esto es porque los pokemon pueden tener hasta dos tipos.

Como nos interesan las relaciones entre tipos individuales, los primero que hacemos es filtrar a todos los pokemon que tienen más de un tipo con la función filter(). Reconocemos a estos caso porque tienen un NA en la columna type2.

Vamos a guardar nuestros resultados en un data frame llamado poke_fuerza.

La información de fortalezas y debilidades se encuentra en todas las columnas que inician con “against_” expresado como un multiplicador.

Por ejemplo, si tenemos un pokemon de tipo fire (fuego) y vemos la columna “against_ground” (contra tierra), veremos un valor de 2.

Esto nos indica que un pokemon de tipo fuego recibe el doble de daño de un pokemon tipo tierra, es decir, fuego es debil contra tierra este tipo, o expresado de manera inversa, tierra es fuerte contra fuego.

Si el valor en la columna es 0.5, entonces este tipo de pokemon recibe la mitad de daño, es fuerte contra él y si el valor es 1, entonces no hay diferencia, no es ni fuerte ni débil contra ese tipo.

Vamos a quedarnos sólo con estas columnas y la que contiene el tipo de pokemon, usando la función select() de dplyr. De una vez, cambiamos el nombre de la columna “type1” a “tipo”.

El formato en el que se encuentran nuestros datos es conocido como un formato “ancho” (wide), pues tenemos distintos valores de un mismo atributo (fortaleza - debilidad) en columnas separadas (las columnas “against_”.

Necesitamos cambiar este formato a uno alto (tall) para generar una tabla apropiada para generar redes relacionales.

Usamos la función gather() de tidyr para esta tarea. Vamos a consolidar todas las columnas “against_” en dos columnas, una llamada “enemigo” que contenga el nombre de las columnas originales y otra llamada “modificador” con los valores numéricos.

Vemos nuestro resultado.

Quitamos “against_” de los datos en la columna "enemigo usando la función gsub() dentro de mutate().

Veamos cuáles tipos tenemos en las columnas tipo y enemigo.

##  [1] "fire"     "water"    "bug"      "poison"   "electric" "fairy"   
##  [7] "fighting" "psychic"  "ground"   "normal"   "grass"    "dragon"  
## [13] "rock"     "dark"     "ghost"    "ice"      "steel"    "flying"
##  [1] "bug"      "dark"     "dragon"   "electric" "fairy"    "fight"   
##  [7] "fire"     "flying"   "ghost"    "grass"    "ground"   "ice"     
## [13] "normal"   "poison"   "psychic"  "rock"     "steel"    "water"

Tenemos un tipo con un nombre inconsistente en “enemigo”. “fight” no es un nombre válido de tipo de pokemon, debemos reemplazarlo por “fighting” usando gsub()

Ya que los tipos son correctos en las columnas “tipo” y “enemigo”, nos quedamos sólo con los renglones únicos, quitando los repetidos con la función distinct() de dplyr.

Por último, transformamos los valores en la columna modificador en etiquetas, que asignamos a una nueva columna llamada “relacion”.

Nos guiamos con las reglas que vimos más arriba, pero debemos considerar la manera en que la relación que describamos será expresada y qué tan clara es.

Retomemos el ejemplo de un pokemon tipo fuego. Si este tiene un valor de modificar de 2 contra un pokemon tipo agua, podríamos representarlo de la siguiente manera.

fuego -- debil -> agua

No hay ningún problema con esto, comunica la relación que existe entre estos dos elementos. Sin embargo, veamos que pasa si cambiamos el orden de los elementos, convirtiendo agua en el origen de la relación y fuego en el destino, y con ello cambiando la relación de “débil” a “fuerte”.

agua -- fuerte -> fuego

Es la misma información representada de una manera diferente, probablemente más clara. Después de todo, explicamos el juego de piedra, papel y tijera como piedra le gana a tijera, tijera le gana a papel, y papel le gana a piedra; no al revés: piedra pierde contra papel, papel pierde contra tijera, y tijera pierde contra piedra.

De nuevo, es la misma información, pero la primera manera de presentarla me parece más intuitiva.

Cambiaremos el orden de las columnas, “enemigo” será la columna de origen y “tipo” la de destino, usando la función select(). Con esto en mente, entonces las reglas para etiquetar las relaciones serán: Si la columna modificador es menor que uno, entonces asignamos la etiqueta “Débil”, si es mayor que uno será “Fuerte”.

Para etiquetar usamos la función ifelse() dentro de mutate().

Por último, como no son de nuestro interés las relaciones neutrales, utilizamos filter() para quitarlas, antes de etiquetar.

Nuestro resultado es el siguiente.

## # A tibble: 120 x 4
##    enemigo tipo     modificador Relacion
##    <chr>   <chr>          <dbl> <chr>   
##  1 bug     fire             0.5 Fuerte  
##  2 bug     poison           0.5 Fuerte  
##  3 bug     fairy            0.5 Fuerte  
##  4 bug     fighting         0.5 Fuerte  
##  5 bug     psychic          2   Débil   
##  6 bug     grass            2   Débil   
##  7 bug     dark             2   Débil   
##  8 bug     ghost            0.5 Fuerte  
##  9 bug     steel            0.5 Fuerte  
## 10 bug     flying           0.5 Fuerte  
## # ... with 110 more rows

Para facilitar la legibilidad de las gráficas que generemos más adelante, vamos a cambiar los valores de “tipo” y “enemigo” a mayúsculas y los truncaremos a sus primeros tres caracteres. Hacemos esto con mutate_at() de dplyr.

Finalmente, transformamos este data frame en un objeto apropiado para generar redes relacionales, con la función graph_from_data_frame() del paquete igraph.

Esta función siempre toma las primeras dos columnas de un data frame como el origen y destino de las relaciones, por lo que debes verificar que la tabla que uses tenga la estructura correcta.

Veamos el resultado y la clase del objeto que hemos creado con graph_from_data_frame().

## IGRAPH cfdd704 DN-- 18 120 -- 
## + attr: name (v/c), modificador (e/n), Relacion (e/c)
## + edges from cfdd704 (vertex names):
##  [1] BUG->FIR BUG->POI BUG->FAI BUG->FIG BUG->PSY BUG->GRA BUG->DAR
##  [8] BUG->GHO BUG->STE BUG->FLY DAR->FAI DAR->FIG DAR->PSY DAR->DAR
## [15] DAR->GHO DRA->FAI DRA->DRA DRA->STE ELE->WAT ELE->ELE ELE->GRO
## [22] ELE->GRA ELE->DRA ELE->FLY FAI->FIR FAI->POI FAI->FIG FAI->DRA
## [29] FAI->DAR FAI->STE FIG->BUG FIG->POI FIG->FAI FIG->PSY FIG->NOR
## [36] FIG->ROC FIG->DAR FIG->GHO FIG->ICE FIG->STE FIG->FLY FIR->FIR
## [43] FIR->WAT FIR->BUG FIR->GRA FIR->DRA FIR->ROC FIR->ICE FIR->STE
## [50] FLY->BUG FLY->ELE FLY->FIG FLY->GRA FLY->ROC FLY->STE GHO->PSY
## + ... omitted several edges
## [1] "igraph"

Podemos realizar todo lo anterior en un sólo proceso.

Ahora que tenemos un objeto de clase igraph que podemos crear visualizaciones de redes relacionales. Para ello, tenemos dos métodos:

Revisemos ambos métodos.

Generación de redes relacionales usando plot()

Para crear la visualización de una red relacional con plot(), debemos cargar primero el paquete igraph, de este modos se anexan nuevos métodos a esta función.

Hecho esto, basta con usar nuestro objeto igraph como argumento para plot().

Obtenemos una visualización bastante aceptable considerando el mínimo esfuerzo invertido en crearla. Tenemos una buena representación de las uniones y nodos de nuestra red, así como de la direccionalidad de las uniones.

Podemos ajustar la apariencia de una gráfica generada con este método usando distintos parámetros. Los parámetros que modifican las uniones entre nodos inician con la palabra “edge” y las que afectan a los nodos, con la palabra “vertex”.

Por ejemplo, cambiamos el color de las uniones con los argumentos edge.color y el tamaño de la cabeza de flecha en ellas con edge.arrow.size. Modificamos el color de los nodos con vertex.color, el texto en ellos con vertex.label.color y su tamaño con vertex.size.

Podemos cambiar la manera en que están distribuidos los nodos usando como argumento layout una de las funciones deigraph específica para esta tarea.

Por ejemplo, para graficar con una distribución en forma de círculo usamos la función layout.circle().