Soy aficionado a los superhéroes. Muy aficionado. Hasta podría atribuir mi gusto a la lectura a los cómics de Superman y Batman cuando era pequeño, y que haya sobrevivido a la pubertad y adolescencia a los cómics de los X-Men.

Así que, cuando me encontré con un conjunto de datos con información de superhéroes y sus poderes, se me ocurrió que sería el pretexto perfecto para hablar sobre el Análisis de Componentes Principales y cómo podemos usarlo para caracterizar o clasificar datos.

Hasta podríamos ser ambiciosos y decir que esta es una forma no supervisada de aprendizaje automático, pero nos basta saber que con Análisis de Componentes Principales tenemos una herramienta para entender y describir mejor nuestros datos.

En este documento revisaremos como implementar el Análisis de Componentes Principales usando el paquete psych de R, y de paso aprenderemos un poco más sobre los superhéroes de DC Comics y Marvel Comics.

Una introducción (muy) informal al Análisis de Componentes Principales

El Análisis de Componentes Principales es, en realidad, un procedimiento bastante complejo que involucra álgebra lineal y tiene diferentes usos e interpretaciones dependiendo del campo de aplicación.

Introducciones formales al PCA pueden ser encontradas en los siguientes enlaces:

Nosotros utilizaremos PCA como una manera de encontrar una estructura subyacente a nuestros datos. Específicamente, vamos a explorar la posibilidad de que los poderes de los superhéroes de DC y Marvel forman grupos que pueden caracterizar a los personajes.

Partimos de tres supuesto generales.

Es decir, suponemos hay poderes relacionados entre sí, por ejemplo, volar y tener fuerza sobrehumana o súper velocidad y resistencia física sobrehumana.

Suponemos que esos poderes pueden agruparse entre sí, y que esos grupos no son iguales. Podríamos decir que esos grupos, o componentes, corresponden a un arquetipo de superhéroe. En este ejemplo, poderes de “superhumano” y poderes de “velocista”.

Entonces, con estos componentes podríamos clasificar a nuestros superhéroes en diferentes arquetipos, Flash como “velocista”, Sentry como “superhumano”, etcétera.

Veamos si lo logramos, empezando por preparar nuestro entorno de trabajo.

Paquetes necesarios

Estos son los paquetes que utilizaremos.

library(tidyverse)
library(psych)

Si no cuentas con estos paquetes, puedes instalarlos con install.packages().

Lectura de datos

Usaremos el conjunto de datos “Super Hero Dataset”, disponible en Kaggle.

He alojado una copia de estos datos en Github que puede ser descargada usando download.file()

download.file("https://github.com/jboscomendoza/rpubs/raw/master/pca_superheroes/superhero-set.zip", destfile = "superhero-set.zip")

De esta manera obtenemos un archivo .zip. Extraemos su contenido en nuestra carpeta de trabajo con unzip().

unzip("superhero-set.zip")

Esto nos dejará con dos archivos:

Importación de los datos

Para este análisis, usaremos los datos de sólo dos editoriales: DC Comics y Marvel Comics. Esto, por dos razones.

En primer lugar, porque estas dos editoriales son las más grandes y tienen una larga tradición publicando cómics de superhéroes, sus personajes tienden a seguir una línea editorial más o menos consistente; en segundo lugar, dado que conozco mejor a los personajes de estas dos editoriales, es más fácil que interprete los resultados y juzgue si tienen sentido o no. Como siempre, el conocimiento disciplinar es importante.

Empezamos por importar la información de los superhéroes.

Usamos la función read_csv() de readr para leer los archivos .csv, después select() de dplyr para elegir las columnas que conservaremos (nombre del personaje y editorial), y por último filter(), también de dplyr para filtrar sólo los datos de las editoriales DC y Marvel.

dc_marvel <-
  read_csv("heroes_information.csv") %>%
  select(name, Publisher) %>%
  filter(Publisher %in% c("DC Comics", "Marvel Comics"))
## Warning: Missing column names filled in: 'X1' [1]
## Parsed with column specification:
## cols(
##   X1 = col_integer(),
##   name = col_character(),
##   Gender = col_character(),
##   `Eye color` = col_character(),
##   Race = col_character(),
##   `Hair color` = col_character(),
##   Height = col_double(),
##   Publisher = col_character(),
##   `Skin color` = col_character(),
##   Alignment = col_character(),
##   Weight = col_double()
## )
# Resultados
dc_marvel
## # A tibble: 603 x 2
##    name          Publisher    
##    <chr>         <chr>        
##  1 A-Bomb        Marvel Comics
##  2 Abin Sur      DC Comics    
##  3 Abomination   Marvel Comics
##  4 Abraxas       Marvel Comics
##  5 Absorbing Man Marvel Comics
##  6 Adam Strange  DC Comics    
##  7 Agent 13      Marvel Comics
##  8 Agent Bob     Marvel Comics
##  9 Agent Zero    Marvel Comics
## 10 Air-Walker    Marvel Comics
## # ... with 593 more rows

Importamos los poderes de los personajes.

heroe_poderes <- read_csv("super_hero_powers.csv")
## Parsed with column specification:
## cols(
##   .default = col_character()
## )
## See spec(...) for full column specifications.
# Resultados
heroe_poderes
## # A tibble: 667 x 168
##    hero_names  Agility `Accelerated Hea~ `Lantern Power ~ `Dimensional Aw~
##    <chr>       <chr>   <chr>             <chr>            <chr>           
##  1 3-D Man     True    False             False            False           
##  2 A-Bomb      False   True              False            False           
##  3 Abe Sapien  True    True              False            False           
##  4 Abin Sur    False   False             True             False           
##  5 Abomination False   True              False            False           
##  6 Abraxas     False   False             False            True            
##  7 Absorbing ~ False   False             False            False           
##  8 Adam Monroe False   True              False            False           
##  9 Adam Stran~ False   False             False            False           
## 10 Agent Bob   False   False             False            False           
## # ... with 657 more rows, and 163 more variables: `Cold Resistance` <chr>,
## #   Durability <chr>, Stealth <chr>, `Energy Absorption` <chr>,
## #   Flight <chr>, `Danger Sense` <chr>, `Underwater breathing` <chr>,
## #   Marksmanship <chr>, `Weapons Master` <chr>, `Power
## #   Augmentation` <chr>, `Animal Attributes` <chr>, Longevity <chr>,
## #   Intelligence <chr>, `Super Strength` <chr>, Cryokinesis <chr>,
## #   Telepathy <chr>, `Energy Armor` <chr>, `Energy Blasts` <chr>,
## #   Duplication <chr>, `Size Changing` <chr>, `Density Control` <chr>,
## #   Stamina <chr>, `Astral Travel` <chr>, `Audio Control` <chr>,
## #   Dexterity <chr>, Omnitrix <chr>, `Super Speed` <chr>,
## #   Possession <chr>, `Animal Oriented Powers` <chr>, `Weapon-based
## #   Powers` <chr>, Electrokinesis <chr>, `Darkforce Manipulation` <chr>,
## #   `Death Touch` <chr>, Teleportation <chr>, `Enhanced Senses` <chr>,
## #   Telekinesis <chr>, `Energy Beams` <chr>, Magic <chr>,
## #   Hyperkinesis <chr>, Jump <chr>, Clairvoyance <chr>, `Dimensional
## #   Travel` <chr>, `Power Sense` <chr>, Shapeshifting <chr>, `Peak Human
## #   Condition` <chr>, Immortality <chr>, Camouflage <chr>, `Element
## #   Control` <chr>, Phasing <chr>, `Astral Projection` <chr>, `Electrical
## #   Transport` <chr>, `Fire Control` <chr>, Projection <chr>,
## #   Summoning <chr>, `Enhanced Memory` <chr>, Reflexes <chr>,
## #   Invulnerability <chr>, `Energy Constructs` <chr>, `Force
## #   Fields` <chr>, `Self-Sustenance` <chr>, `Anti-Gravity` <chr>,
## #   Empathy <chr>, `Power Nullifier` <chr>, `Radiation Control` <chr>,
## #   `Psionic Powers` <chr>, Elasticity <chr>, `Substance Secretion` <chr>,
## #   `Elemental Transmogrification` <chr>, `Technopath/Cyberpath` <chr>,
## #   `Photographic Reflexes` <chr>, `Seismic Power` <chr>, Animation <chr>,
## #   Precognition <chr>, `Mind Control` <chr>, `Fire Resistance` <chr>,
## #   `Power Absorption` <chr>, `Enhanced Hearing` <chr>, `Nova
## #   Force` <chr>, Insanity <chr>, Hypnokinesis <chr>, `Animal
## #   Control` <chr>, `Natural Armor` <chr>, Intangibility <chr>, `Enhanced
## #   Sight` <chr>, `Molecular Manipulation` <chr>, `Heat Generation` <chr>,
## #   Adaptation <chr>, Gliding <chr>, `Power Suit` <chr>, `Mind
## #   Blast` <chr>, `Probability Manipulation` <chr>, `Gravity
## #   Control` <chr>, Regeneration <chr>, `Light Control` <chr>,
## #   Echolocation <chr>, Levitation <chr>, `Toxin and Disease
## #   Control` <chr>, Banish <chr>, `Energy Manipulation` <chr>, `Heat
## #   Resistance` <chr>, ...

Como en este segundo conjunto de datos no tenemos un identificador de editorial, filtramos utilizando los datos de dc_marvel para quedarmos con los personajes de DC y Marvel.

heroe_poderes <-
  heroe_poderes %>%
  filter(hero_names %in% dc_marvel$name)

El siguiente paso es procesar nuestros datos para el análisis.

Procesamiento de los datos

Los nombres de nuestros datos nos darán problemas más adelantes si los dejamos como están. Los espacios en los nombres de columnas pueden producir errores o comportamientos imprevistos, así que los quitaremos, lo mismo que el resto de signos de puntuación. Ambos serán reemplazados por guiones bajos (“_“) usando Regular Expressions (regex) y la función gsub().

names(heroe_poderes) <-
  names(heroe_poderes) %>%
  tolower() %>%
  gsub("\\W+", "_", .)

Creamos dos data frames diferentes, una con los nombres de los personajes y otra con los poderes.

# Personajes
heroe <- select(heroe_poderes, hero_names)

# Poderes
poderes <- select(heroe_poderes, -hero_names)

Poderes escasos y abundantes

Como vimos más arriba, los poderes que tienen los personajes están codificados como cadenas de texto “True” y “False”, recodificamos a 1 y 0, respectivamente, para poder hacer cálculos numéricos.

poderes <-
  map_df(poderes, ~ifelse(. == "True", 1, 0))

Hecho esto, podemos calcular cuántos personajes tienen un poder en particular a partir de la suma de valores de una columna. Veamos cuántos personajes tienen “grito sónico”.

sum(poderes$sonic_scream)
## [1] 5

Podríamos apostar a que uno de ellos es “Banshee”.

Usando map() de dplyr, obtenemos la suma anterior para todos los poderes. De esto sabremos cuáles poderes son los más y menos comunes. Con esta información podremos detectar outliers: poderes muy raros o muy comunes.

Si omitimos estos poderes al realizar PCA obtendremos mejores resultados, pues los este método es sensible a valores extremos.

index_poderes <- map_dbl(poderes, sum)

Veamos la distribución de los poderes con una curva de densidad.

plot(density(index_poderes), main = "Frecuencia de poderes")

Ahora veamos los cinco poderes más y menos comunes, con ayuda de sort() y head().

# Más comunes
head(sort(index_poderes, decreasing = TRUE), 5)
## super_strength        stamina     durability    super_speed         flight 
##            302            228            215            207            191
# Menos comunes
head(sort(index_poderes), 5)
##       hyperkinesis     thirstokinesis     changing_armor 
##                  0                  0                  0 
##  spatial_awareness intuitive_aptitude 
##                  0                  0

Tenemos poderes que ningún personaje en nuestros datos posee, así que podemos omitirlos sin ningún problema. También podemos omitir aquellos poderes que sólo aparecen en una ocasión, pues es probable que tampoco aporten mucha información.

También tenemos poderes que más de 200 personajes poseen. Esto es casi 30% de nuestros personajes. Estos poderes quizás no ayuden a crear grupos distintos entre sí, así que haremos el análisis sin ellos.

La manera de quitar estos poderes es un poco enredada, pero consiste en usar sus índices de posición en el vector index_poderes, para así hacer una selección de columnas a conservar.

poderes <- poderes[(index_poderes > 4 & index_poderes < 150)]

# Tamaño nuevo de poderes
dim(poderes)
## [1] 521 116
# Histograma nuevo de poderes
map_dbl(poderes, sum) %>% 
  density() %>% 
  plot(main = "Frecuencia de poderes")

Personajes muy poderosos

También quitaremos a los personajes que no tiene ningún poder y a aquellos que tienen un número muy alto de ellos. Estos datos también pueden ser considerados como outliers.

Para descubrir quienes son estos personajes, usamos la función rowSums().

index_heroe <- rowSums(poderes)

# Resutlado
index_heroe
##   [1]  4  1  5 11  9  7  1  5  3  2 11 38  1 12  0  4  5  1  2  6 20 11 17
##  [24]  2  9 18  6  3  0 12  3  2  2  7  3  1  3  1  1  5  6  2  8  2  4  0
##  [47]  6  4  2 12  6  1  5  2 10  9  1  4 15  5  1  2  2  2  1  6 16  5  9
##  [70]  7  7  3  8  1  6  3 13 11  7  5  1  2  7  2  1  2  1  1  8  6  8  1
##  [93]  1  9  5 14  1  7  3  1  2  7  1  1 23  5  3  7 22  4  2 26  4 21  7
## [116] 11  2  6  3  0  4  1  3  3  2 13  3  5  5  5  0  3  1  4  7 13  6  3
## [139]  9  6 23  4 13  9 14  3  6  7  4  4  2  0  8  3 19  6 17  3  0 14  5
## [162] 10 24  7  1  1  9  3 11 11  8 14  7 11  1  3 14 14  6  1  6  6  4  5
## [185] 19  8 10 10  6  2 21  3  1 24  4  8  1 14  1 14  3  2 10  2  1  1 10
## [208]  0  4  4  5  4  0  0 10  7 10  7  5  3  4  2  5  1  1 11  5  6  4  1
## [231]  2  7 13  8  2  3 16  7 12 12  8  5  3  7 16 16  2  5  5 12  2  8  1
## [254]  4 10  3  6  2  3  5  4  4  6  5  7  1  8  5  4 13  5  3  2 10  9  1
## [277]  6  3  0 19  7  2  1  2  1 30 10 10  9  4  3  1  3 13  1  1  6  2  8
## [300]  6  9  5 11 12 29 10 11  1  5  2  6 13 11  5  4  4  1  6  2 12  4  4
## [323]  2 29 18  4  2  5  4  4  2  0  3  0  1 11 10 11  5  3  1  2  2  6  6
## [346]  2 17 25 10 23 19  1  4  6  7  1  1  3  1 16  3  9  1  8  7 18  8  3
## [369]  1 10  6  5  1  1  2  2  3  4 12  2  3  5  7  4  1  8  4  1  1  7  2
## [392]  5  3  4  4  1  9  2  3 13  4  9  2  2 10  6 12  3  5  2 17  6  3  8
## [415]  3  2  5  2  4  7  7 12  1  1 14  3 10  4 11  2  2  3  6  0  7  2  9
## [438] 44  4  4 11 11 15  6  4  7  5  5  9  4  9 11  4 10  7  9 11 18 21 23
## [461] 12  7  1  6  3 25  1  6 11  4  0  3  3  1 11  2  1 13 15  0 10  1  4
## [484]  3 18  3  1  1 10  5  8 21  1  4  6 15 12  4  2 14  3  1  6  6 17  4
## [507]  3  6  4  6 17  4  6 25 13  9  2  2  4  7  3

Obtenemos una vector en el que cada valor representa el total de poderes por renglón, esto es, los poderes que posee cada personaje.

Demos un vistazo a cómo se distribuyen estos valores.

plot(density(index_heroe), "Distribución de héroe")