La exploración de nuestros datos es un paso esencial para cualquier tipo de análisis que deseemos realizar. Si no conocemos la estructura de nuestros datos, sus propiedades y particularidades, después podemos encontrarnos con problemas para analizar, modelar e interpretar resultados.

No importa que tan sofisticada sea una técnica de modelo estadístico o aprendizaje automático, si no sabemos qué le estamos dando para trabajar, difícilmente sabremos qué obtendremos de ellas.

Para este documento utilizaremos datos de ventas de videojuegos y partiremos con una pregunta

Es una pregunta que “sólo” implica describir nuestros datos. No hay predicciones, clasificación ni otras tareas complejas pero, como veremos, darle una buena respuesta implica observación, reflexión y adaptación.

Empecemos preparando nuestro entorno de trabajo.

Preparación

Para este análisis usaremos R, con el meta paquete tidyverse y el paquete scales.

library(tidyverse)
library(scales)

Además definiremos un tema para ggplot2. Es theme_minimal() con un par de cambios.

theme_graf <- 
  theme_minimal() +
  theme(text = element_text(family = "serif", size = 14),
        axis.text = element_text(size = 12), 
        panel.grid.minor = element_blank(),
        legend.position = "top")

Con esto estamos listos para empezar.

Descarga y lectura de los datos

Descargamos los datos desde Kaggle usando la siguiente dirección (requiere login):

Una vez descargado el archivo, extraemos su contenido, lo leemos y lo asignamos al objeto vgsales.

unzip("Video_Games_Sales_as_at_22_Dec_2016.csv.zip")

vgsales <- read_csv("Video_Games_Sales_as_at_22_Dec_2016.csv")
## Parsed with column specification:
## cols(
##   Name = col_character(),
##   Platform = col_character(),
##   Year_of_Release = col_character(),
##   Genre = col_character(),
##   Publisher = col_character(),
##   NA_Sales = col_double(),
##   EU_Sales = col_double(),
##   JP_Sales = col_double(),
##   Other_Sales = col_double(),
##   Global_Sales = col_double(),
##   Critic_Score = col_integer(),
##   Critic_Count = col_integer(),
##   User_Score = col_character(),
##   User_Count = col_integer(),
##   Developer = col_character(),
##   Rating = col_character()
## )

Es importante mencionar que de acuerdo a la documentación de estos datos:

Esto último es importante, pues cambia la manera en que interpretamos nuestros resultados. Juegos con más edad tienen la posibilidad de haber vendido unidades durante más de un año.

Para fines de simplificar este documento, asumiremos que las ventas de un juego ocurren sólo ocurren en el año que fue publicado.

Limpieza inicial de los datos

Usaremos summary() para explorar nuestros datos. Veremos las variables que tenemos, de qué clase son y algunas medidas de resumen.

summary(vgsales)
##      Name             Platform         Year_of_Release   
##  Length:16719       Length:16719       Length:16719      
##  Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character  
##                                                          
##                                                          
##                                                          
##                                                          
##     Genre            Publisher            NA_Sales          EU_Sales     
##  Length:16719       Length:16719       Min.   : 0.0000   Min.   : 0.000  
##  Class :character   Class :character   1st Qu.: 0.0000   1st Qu.: 0.000  
##  Mode  :character   Mode  :character   Median : 0.0800   Median : 0.020  
##                                        Mean   : 0.2633   Mean   : 0.145  
##                                        3rd Qu.: 0.2400   3rd Qu.: 0.110  
##                                        Max.   :41.3600   Max.   :28.960  
##                                                                          
##     JP_Sales        Other_Sales        Global_Sales      Critic_Score  
##  Min.   : 0.0000   Min.   : 0.00000   Min.   : 0.0100   Min.   :13.00  
##  1st Qu.: 0.0000   1st Qu.: 0.00000   1st Qu.: 0.0600   1st Qu.:60.00  
##  Median : 0.0000   Median : 0.01000   Median : 0.1700   Median :71.00  
##  Mean   : 0.0776   Mean   : 0.04733   Mean   : 0.5335   Mean   :68.97  
##  3rd Qu.: 0.0400   3rd Qu.: 0.03000   3rd Qu.: 0.4700   3rd Qu.:79.00  
##  Max.   :10.2200   Max.   :10.57000   Max.   :82.5300   Max.   :98.00  
##                                                         NA's   :8582   
##   Critic_Count     User_Score          User_Count       Developer        
##  Min.   :  3.00   Length:16719       Min.   :    4.0   Length:16719      
##  1st Qu.: 12.00   Class :character   1st Qu.:   10.0   Class :character  
##  Median : 21.00   Mode  :character   Median :   24.0   Mode  :character  
##  Mean   : 26.36                      Mean   :  162.2                     
##  3rd Qu.: 36.00                      3rd Qu.:   81.0                     
##  Max.   :113.00                      Max.   :10665.0                     
##  NA's   :8582                        NA's   :9129                        
##     Rating         
##  Length:16719      
##  Class :character  
##  Mode  :character  
##                    
##                    
##                    
## 

User_Score y Year_of_Release aparecen como datos de tipo character, cuando la lógica nos indica deberían ser ser numéricos. Usemos table() para ver que ocurre al interior de estas variables.

table(vgsales["User_Score"])
## 
##    0  0.2  0.3  0.5  0.6  0.7  0.9    1  1.1  1.2  1.3  1.4  1.5  1.6  1.7 
##    1    2    2    2    2    2    2    2    2    3    2    5    2    3    9 
##  1.8  1.9    2  2.1  2.2  2.3  2.4  2.5  2.6  2.7  2.8  2.9    3  3.1  3.2 
##    6    2   11    9    6    2   11   12    4    8   24    9   21   23   13 
##  3.3  3.4  3.5  3.6  3.7  3.8  3.9    4  4.1  4.2  4.3  4.4  4.5  4.6  4.7 
##   15   23   26   20   19   28   13   27   33   28   37   33   34   37   24 
##  4.8  4.9    5  5.1  5.2  5.3  5.4  5.5  5.6  5.7  5.8  5.9    6  6.1  6.2 
##   49   40   64   44   57   70   72   71   72   78   97   77  127   84  113 
##  6.3  6.4  6.5  6.6  6.7  6.8  6.9    7  7.1  7.2  7.3  7.4  7.5  7.6  7.7 
##  138  107  125  148  128  197  143  220  180  167  236  215  251  220  240 
##  7.8  7.9    8  8.1  8.2  8.3  8.4  8.5  8.6  8.7  8.8  8.9    9  9.1  9.2 
##  324  249  290  244  282  254  223  253  211  188  186  153  120   90   43 
##  9.3  9.4  9.5  9.6  9.7  tbd 
##   31   11    6    2    1 2425
table(vgsales["Year_of_Release"])
## 
## 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 
##    9   46   36   17   14   14   21   16   15   17   16   41   43   62  121 
## 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 
##  219  263  289  379  338  350  482  829  775  762  939 1006 1197 1427 1426 
## 2010 2011 2012 2013 2014 2015 2016 2017 2020  N/A 
## 1255 1136  653  544  581  606  502    3    1  269

El problema está en que User_Score tiene un valor no númerico, “tbd”, mientras que en Year_of_Release los años no disponibles fueron codificados como “N/A”. Estas cadenas de texto cambian todos los valores en esas variables a tipo character().

Para corregirlo, en ambos casos, podemos reemplazar esos datos anomalos por NA y convertir a numérico.

Además, aprovechando que haremos modificaciones a los datos, uniformaremos la escala de puntuación de Critic_Score y User_Score. Una de ellas va de 1 a 10, y la otra de 1 a 100. Transformaremos User_Score para que ambas tengan valores de 1 a 100.

vgsales <-
  vgsales %>%
  mutate(
    User_Score = ifelse(User_Score == "tbd", NA, as.numeric(User_Score)),
    User_Score = User_Score * 10,
    Year_of_Release = ifelse(Year_of_Release == "N/A", NA, Year_of_Release),
    Year_of_Release = as.numeric(as.character(Year_of_Release))
  ) %>%
  mutate_if(is.integer, as.numeric)
## Warning in ifelse(User_Score == "tbd", NA, as.numeric(User_Score)): NAs
## introducidos por coerción

Ahora pasamos a procesar las variables que más nos interesan: Year_of_Release (fecha de salida del juego) y Global_Sales (ventas mundiales).

Procesando la fecha de salida

Como desamos ver cambios a través del tiempo, nos conviene tener completa la información de Year_of_Release. Por desgracia, tenemos datos perdidos. Veamos cuántos son.

# Total perdidos
sum(is.na(vgsales[["Year_of_Release"]]))
## [1] 269
# Proporcion perdidos
(sum(is.na(vgsales[["Year_of_Release"]])) / length(vgsales[["Year_of_Release"]])) * 100
## [1] 1.608948

La proporción de datos perdidos que tenemos es apenas mayor a 1%. Probablemte esto no represente un problema serio, pero hagamos un esfuerzo para quedarnos con la menor cantidad posible de datos perdidos.

Empecemos por buscar patrones en nuestros datos que nos ayuden a llenar esos datos perdidos.

vgsales %>%
  filter(is.na(Year_of_Release))
## # A tibble: 269 x 16
##    Name      Platform Year_of_Release Genre  Publisher   NA_Sales EU_Sales
##    <chr>     <chr>              <dbl> <chr>  <chr>          <dbl>    <dbl>
##  1 Madden N~ PS2                   NA Sports Electronic~    4.26     0.260
##  2 FIFA Soc~ PS2                   NA Sports Electronic~    0.590    2.36 
##  3 LEGO Bat~ Wii                   NA Action Warner Bro~    1.80     0.970
##  4 wwe Smac~ PS2                   NA Fight~ N/A            1.57     1.02 
##  5 Space In~ 2600                  NA Shoot~ Atari          2.36     0.140
##  6 Rock Band X360                  NA Misc   Electronic~    1.93     0.330
##  7 Frogger'~ GBA                   NA Adven~ Konami Dig~    2.15     0.180
##  8 LEGO Ind~ Wii                   NA Action LucasArts      1.51     0.610
##  9 Call of ~ Wii                   NA Shoot~ Activision     1.17     0.840
## 10 Rock Band Wii                   NA Misc   MTV Games      1.33     0.560
## # ... with 259 more rows, and 9 more variables: JP_Sales <dbl>,
## #   Other_Sales <dbl>, Global_Sales <dbl>, Critic_Score <dbl>,
## #   Critic_Count <dbl>, User_Score <dbl>, User_Count <dbl>,
## #   Developer <chr>, Rating <chr>

Hora del conocimiento disciplinar

Por fortuna, tengo un mínimo de conocimiento básico sobre videojuegos, que puedo aprovechar para la tarea de reducir datos perdidos.

En primer lugar, sé que todos los juegos deben tener una fecha de salida para poder venderse. Suena a una observación boba, pero esto quiere decir que todos los datos perdidos en el año de salida de los juegos deben tener un valor verdadero, que desconocemos. NA no es un valor que pueda ocurrir en el mundo real.

También es posible reconocer que algunos de los juegos sin año son títulos que aparecieron en varias plataformas, como es el caso de Rock Band. Aunque probablemente no siempre sea el caso, podemos asumir que versiones de un mismo juego que ahora no tienen fecha, aparecieron en años similares que las demás versiones.

Por último, hay juegos del género de deportes (Sports) y carreras (Racing) cuyo nombre hace referencia al año en que salieron a la venta. Un ejemplo es Madden NFL 2004. En este caso, y de nuevo aplicando el poco conocimiento del tema que tengo, sé que los juegos de deportes con una año en su nombre, en realidad som puestos a la venta el año anterior. Por lo tanto, podríamos introducir el Year_of_Release de Madden NFL 2004 como 2003.

Versiones de un mismo juego

Empecemos a aplicar nuestro conocimiento disciplinar imputando los años de versiones de un mismo juego.

vgsales <-
  vgsales %>%
  group_by(Name) %>%
  mutate(
    Versions = n(),
    Imputed_Year = round(median(Year_of_Release, na.rm = T)),
    Year_of_Release = ifelse(is.na(Year_of_Release), Imputed_Year, Year_of_Release),
    Year_of_Release = ifelse(is.nan(Year_of_Release), NA, Year_of_Release)
  ) %>%
  ungroup()

Deportes y carreras

Para los juegos de deportes y carreras, recogeremos información de sus nombres para imputar el año de salida. Esto incluye tomar en cuenta maneras alternas de escribir fechas (“2K6” para referirse a “2006”), así como aquellos que aparecen con una fecha de dos dígitos(“99” en lugar de “1999”) en lugar de cuatro, que es el formato de Year_of_Release.

Gurdaremos los resultados de este proceso en el objeto sports_years.

sport_years <-
  vgsales %>%
  filter(is.na(Year_of_Release)) %>%
  mutate(year_foo = str_extract(Name, "\\d+$")) %>%
  filter(!is.na(year_foo) & Genre %in% c("Racing", "Sports")) %>%
  mutate(
    year_foo = ifelse(grepl("2K", Name), paste0(200, year_foo), year_foo),
    year_foo = ifelse(nchar(year_foo) < 2, NA, year_foo),
    year_foo = ifelse(year_foo %in% 85:99, paste0(19, year_foo), year_foo),
    year_foo = ifelse(year_foo %in% paste0("0", 1:9), paste0(20, year_foo), year_foo),
    year_foo = ifelse(year_foo %in% paste0("1", 1:9), paste0(20, year_foo), year_foo),
    year_foo = ifelse(nchar(year_foo) < 4, NA, year_foo),
    year_foo = as.numeric(year_foo) - 1
  ) %>%
  filter(!is.na(year_foo)) %>%
  select(Name, year_foo)

Combiamos el objeto anterior con nuestros datos.

vgsales[vgsales[["Name"]] %in% sport_years[["Name"]], "Year_of_Release"] <-
  sport_years[["year_foo"]]

Con esto reducimos a más o menos la mitad los datos perdidos en Year_of_Release.

# Total
sum(is.na(vgsales[["Year_of_Release"]]))
## [1] 139
# Porcentaje
sum(is.na(vgsales[["Year_of_Release"]])) / length(vgsales[["Year_of_Release"]])
## [1] 0.008313894

La vida de las consolas

Podemos usar métodos sofisticados para imputar los años faltantes, pero esta ocasión quiero usar un un procedimiento sencillo que, de nuevo, aprovecha lo poco que sé sobre videojuegos.

Sé que los videojuegos de una plataforma (variable Platform) debieron salir a la venta mientras aún se producían juegos para ella, por lo tanto, usaremos la mediana de Year_of_Release como año.

Esto sin duda nos va a dar datos incorrectos, en especial en plataformas que han tenido una vida activa muy larga, como es el caso de PC, pero considerando que

Guardemos en originales los juegos con año faltante.

originales <- vgsales[["Name"]][is.na(vgsales[["Year_of_Release"]])]

Imputemos con la mediana.

vgsales <-
  vgsales %>%
  group_by(Platform) %>%
  mutate(
    Year_of_Release = ifelse(Year_of_Release > 2017, 2017, Year_of_Release),
    Year_of_Release = ifelse(is.na(Year_of_Release), round(median(Year_of_Release, na.rm = T)), Year_of_Release)
  ) %>%
  ungroup

Con esto tenemos nuestra variable Year_of_Release sin datos perdidos. Con este método es casi seguro que tendremos datos erroneos en cuanto a año de salida de los juegos, lo cual podría sesgar nuestros resultados.

Sin embargo, en este análisis nos interesa conocer la tendencia de los datos a largo plazo y para este fin tener menos de 1% de errores no debería representar un problema.

Podemos ver cuántos datos hemos imputado por año y que porcentaje representan con respecto al total.

vgsales %>%
  group_by(Year_of_Release) %>% 
  mutate(Total = n()) %>% 
  filter(Name %in% originales) %>%
  transmute(Imputado = length(Year_of_Release), Proporcion = (Imputado / Total) * 100) %>% 
  ungroup() %>% 
  arrange(Year_of_Release) %>% 
  distinct()
## Adding missing grouping variables: `Year_of_Release`
## # A tibble: 12 x 3
##    Year_of_Release Imputado Proporcion
##              <dbl>    <int>      <dbl>
##  1           1982.       16     30.8  
##  2           1997.        1      0.345
##  3           1998.        6      1.55 
##  4           1999.        1      0.293
##  5           2003.       13      1.63 
##  6           2004.       24      3.03 
##  7           2008.       22      1.50 
##  8           2009.       25      1.72 
##  9           2010.       14      1.09 
## 10           2011.       11      0.945
## 11           2013.        5      0.906
## 12           2014.        1      0.172

El año con el que podríamos tener más problemas es con 2004, por tener 3% de imputaciones con este método, el resto se encuentra alrededor de 1%.

Por supuesto, también podríamos haber omitido estos datos, pero eso es mucho menos interesante.

Cerrando el análisis a 20 años

Un paso sencillo pero necesario para limitar nuestro análisis a ventas de los últimos 20 años que tenemos datos.

vgsales <- 
  vgsales %>% 
  filter(Year_of_Release %in% 1996:2016)

Procesando las ventas globales

Ahora procesaremos la columna Global_Sales, que contiene el total de ventas de cada juego. Esta variable no tiene problema de datos perdidos, así que procedamos a ver su distribución.

vgsales %>%
  ggplot() +
  aes(Global_Sales) +
  geom_density() +
  scale_x_continuous(expand = c(0, 0), breaks = seq(0, 85, by = 5)) +
  scale_y_continuous(expand = c(0, 0)) +
  theme_graf

Tenemos una distribución con datos extremos (outliers), que en este caso deben ser juegos con ventas inusualmente altas que le dan forma a nuestra distribución de una curva alta, delgada y con una cola a la derecha muy larga.

Viendo la distribución ahora sabemos que la mayoría de los juegos tuvieron ventas por debajo de 5 millones, sin embargo, hay por lo menos un dato de más de 80.

Estos es importante considerarlos en nuestro análisis.

Veamos los deciles de nuestra distribución para entenderla mejor.

quantile(vgsales[["Global_Sales"]], probs = seq(0, 1, by = .1))
##    0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
##  0.01  0.02  0.05  0.07  0.11  0.16  0.24  0.36  0.58  1.15 82.53

El 90% de los juegos tuvieron ventas globales de menos de 1.15 millones. Esto hace interesante al 10% superior en ventas globales. Veamoslo de cerca.

quantile(vgsales[["Global_Sales"]], probs = seq(.9, 1, by = .01))
##     90%     91%     92%     93%     94%     95%     96%     97%     98% 
##  1.1500  1.2500  1.3700  1.5100  1.6900  1.9345  2.2536  2.7900  3.5918 
##     99%    100% 
##  5.2900 82.5300

Si quitamos ese 10% superior, la ditribución luce muy diferente.

vgsales %>%
  #filter(Global_Sales < 3.5198) %>%
  filter(Global_Sales < 1.1500) %>%
  ggplot() +
  aes(Global_Sales) +
  geom_density() +
  scale_x_continuous(expand = c(0, 0), breaks = seq(0, 4, by = .5)) +
  scale_y_continuous(expand = c(0, 0)) +
  theme_graf