Mi juego favorito es es Magic: the Gathering. Este es un juego de cartas coleccionable, en el que armas un mazo, siguiendo ciertas limitaciones, con el objetivo de vencer los mazos de tus oponentes.

Este es un hobbie que requiere de una inversión relativamente alta, comparado con otros, así que pensé que sería buena idea hacer un pequeño proyecto de Data Science usando R y datos del sitio MTG Goldfish para estimar la posibilidades de recuperar mi inversión en Magic, en particular, al comprar una caja de sobres de cartas.

Y para empezar, una breve introducción a cómo funciona la economía de Magic: the Gathering.

1 ¿Cómo funcionan los precios de Magic: the Gathering?

En Magic: the Gathering (Magic), cada año aparecen nuevas cartas a la venta en lo que se denomina expansiones o sets. Estos sets son vendidos en sobres de cartas que contienen, generalmente, 15 cartas al azar. Estos sobres a su vez, pueden ser comprados en cajas que contienen, generalmente, 36 de ellos.

Así que si una persona desea obtener las cartas más recientes de un set comprando sobres, depende en gran medida de la suerte.

Como este es un juego y es coleccionable, hay cartas en cada set que son más difíciles de conseguir, pues la frecuencia con la que aparecen al azar en los sobres es menor. Es decir, son cartas más raras.

En Magic las cartas se clasifican por rareza, de las menos a las más raras:

Además, Magic: the Gathering tiene un sistema de juego organizado, en el que se organizan distintos eventos competitivos, que van desde torneos en tiendas locales hasta eventos competitivos nacionales e internacionales.

Lo anterior tiene como consecuencia que aquellas cartas que son efectivas en juego competitivo, pues incrementan las probabilidades de ganar o forman parte de estrategias exitosas, ven un incremento en su costo, pues su demanda se incrementa.

Así, cartas que son raras de conseguir y tienen alta demanda, tienen precio más alto que cartas comunes o poco usadas en juego competitivo. Por supuesto, hay otros factores que inciden en los precios de las cartas, pero para este artículo basta esta explicación simple.

También es muy importante tener en cuenta que los precios de las cartas, frecuentemente, son expresadas en dólares norteamericanos (USD), que es la moneda que usaremos en este análisis. Además, los precios que usaremos son aquellos correspondientes al 12 de Septiembre del 2018.

1.1 MTG Goldfish

Naturalmente, existen sitios de internet que se encargan de dar seguimiento a los precios de las cartas y sus tendencias. Esta es una información sumamente importante para los jugadores de Magic, pues así conocen el valor de las cartas que poseen y aquellas que desean, para sí tomar decisiones de compra, venta e intercambio.

Uno de estos sitios es MTG Goldfish.

En este sitio se encuentran disponibles información actualizada diariamente de precios de todas las cartas, de todos los sets de Magic, presentados en tablas que son muy convenientes para su análisis usando R.

Además, en este sitio se encuentran excelentes artículos, videos, podcast y artículos a la venta relacionados con Magic. Así que si te gusta Magic o te da curiosidad este juego, no dudes en visitar MTG Goldfish y consumir su contenido, no te arrepentirás… y es una manera de agradecer por la información que generan.

Conociendo todo esto, comencemos el análisis para calcular la probabilidad de recuperar nuestra inversión en una caja de Magic.

2 Estructura del análisis.

Las etapas de nuestro análisis son las siguientes

Algunos de estás son más complejas o largas que otras, pero si abordamos el análisis dividido por pasos, es más fácil tomar decisiones sobre el análisis y corregir errores cuando se presentan.

Establecido esto, preparemos nuestro entorno de trabajo.

3 Paquetes necesarios

Para este proyecto usaremos los siguientes paquetes:

library(tidyverse)
library(rvest)
library(xml2)
library(scales)
library(ggrepel)

Como es usual, puedes instalar estos paquetes usando la función install.packages().

Ahora sí, pasemos a la primera etapa del análisis.

4 Descarga de los precios de un set desde MTG Goldfish

En MTG Goldfish, la información de los precios de un set de Magic es presentada en su propia página, que es identificada usando el código del set.

Los sets de Magic son identificados por un código de tres caracteres, en mayúsculas. Por ejemplo, para el set “Dominaria”, su código es “DOM”, por lo que el URL en el que encontramos sus precios se encuentran en:

Una lista de los códigos de sets de Magic se encuentra disponible en:

Usamos la función download_html() del paquete xml2() para descargar una página de internet a nuestra carpeta de trabajo. Llamaremos a este archivo “goldfish_DOM.html”.

download_html(url = "https://www.mtggoldfish.com/index/DOM",
              file = "goldfish_DOM.html")

Podemos definir una función que nos permita descargar el html que corresponde a un set de manera más sencilla, proporcionando el código de tres letras de este.

descargar_set <- function(clave) {
  mtg_url <- paste0("https://www.mtggoldfish.com/index/", clave)
  mtg_archivo <- paste0("goldfish_", clave, ".html")
  download_html(url = mtg_url, file = mtg_archivo)
}

Hecho esto, importamos el archivo con el html descargado y lo asignamos al objeto mtg_dom.

mtg_dom <- read_html("goldfish_DOM.html")

Si llamamos a este objeto, podremos ver que es un documento con estructura de xml.

mtg_dom
## {xml_document}
## <html xmlns="http://www.w3.org/1999/xhtml">
##  [1] <head>\n<title>Dominaria MTG / MTGO Price History</title>\n<meta na ...
##  [2] <body>\n<img alt="MTGGoldfish" class="layout-print-logo" src="//ass ...
##  [3] <div class="container-fluid layout-container-fluid">\n<div id="erro ...
##  [4] <div class="layout-bottom-ad">\n\n  <div class="ads-container-cdm-z ...
##  [5] <div class="layout-bottom-banner">\n<div class="layout-bottom-conte ...
##  [6] <div class="bottom-shelf">\n<div class="banner-contents">\n<p class ...
##  [7] <div aria-hidden="true" aria-labelledby="Login Dialog" class="modal ...
##  [8] <div aria-hidden="true" aria-labelledby="Important Updates" class=" ...
##  [9] <div aria-hidden="true" aria-labelledby="Card Popup" class="modal f ...
## [10] <div class="layout-typePreferencePopup" id="type-preference-popup"> ...
## [11] <script src="//assets1.mtggoldfish.com/assets/application-fef731ca0 ...
## [12] <script src="//assets1.mtggoldfish.com/assets/google_analytics-b937 ...
## [13] <div id="cdm-zone-end"></div>\n
## [14] <script>\nvar _comscore = _comscore || [];\n_comscore.push({ c1: "2 ...
## [15] <noscript>\n   <img src="//b.scorecardresearch.com/p?c1=2&amp;c2=60 ...
## [16] <script type="text/javascript">\n(function () {\n   var d = new Ima ...
## [17] <noscript>\n   &lt;div&gt;&lt;img src="//secure-us.imrworldwide.com ...

Con este formato no podemos hacer mucho. Necesitamos extraer los datos que contiene y para ello, usaremos las funciones del paquete rvest.

5 Procesamiento de los precios para facilitar el análisis

rvest es un paquete usado para extraer y procesar información de documentos html o xml. En nuestro caso, lo haremos a través del uso de identificadores CSS.

No nos detendremos a explicar qué son los identificadores CSS, pero en términos generales, podemos decir que estos describen los elementos y características de una documento html. Puedes leer más al respecto en el siguiente enlace:

Vamos a recuperar los datos de precios de las cartas, así que necesitamos los identificadores CSS de estos datos en particular.

Para obtener los identificadores hay distintos procedimientos. Puedes usar la función “Inspeccionar elemento” de tu navegador de internet y explorar la estructura del documento.

También puedes usar el sitio Selector Gadget para recuperar los identificadores CSS:

Para fines de este artículo, ya he realizado esta tarea. los identificadores CSS que nos interesan son: “.index-price-table-paper tbody tr”.

Con esta información, usamos la función html_node() de rvest().

mtg_dom %>% 
  html_node(css = ".index-price-table-paper tbody tr")
## {xml_node}
## <tr>
## [1] <td class="card"><a data-full-image="https://cdn1.mtggoldfish.com/im ...
## [2] <td>DOM</td>
## [3] <td>Mythic</td>
## [4] <td class="text-right">\n46.99\n</td>
## [5] <td class="text-right">\n<div class="common-price-change">\n-1.89\n\ ...
## [6] <td class="text-right">-4.00%</td>
## [7] <td class="text-right">\n<div class="common-price-change">\n-0.13\n\ ...
## [8] <td class="text-right">0.00%</td>

Aún no es un formato útil para el análisis necesitamos la función html_text(). Hacer esto nos devolverá una cantidad considerable de texto, así que pediremos que se nos muestren sólo las primeras líneas con head().

mtg_dom %>% 
  html_nodes(css = ".index-price-table-paper tbody tr") %>% 
  html_text() %>% 
  head()
## [1] "Teferi, Hero of Dominaria\nDOM\nMythic\n\n46.99\n\n\n\n-1.89\n\n\n\n\n\n-4.00%\n\n\n-0.13\n\n\n\n\n\n0.00%\n"
## [2] "Karn, Scion of Urza\nDOM\nMythic\n\n31.54\n\n\n\n-0.14\n\n\n\n\n\n0.00%\n\n\n-0.14\n\n\n\n\n\n0.00%\n"       
## [3] "Lyra Dawnbringer\nDOM\nMythic\n\n13.00\n\n\n\n+0.01\n\n\n\n\n\n0.00%\n\n\n+0.04\n\n\n\n\n\n0.00%\n"          
## [4] "Mox Amber\nDOM\nMythic\n\n11.42\n\n\n\n-0.06\n\n\n\n\n\n-1.00%\n\n\n-0.09\n\n\n\n\n\n-1.00%\n"               
## [5] "History of Benalia\nDOM\nMythic\n\n10.60\n\n\n\n-0.03\n\n\n\n\n\n0.00%\n\n\n+0.05\n\n\n\n\n\n0.00%\n"        
## [6] "Sulfur Falls\nDOM\nRare\n\n4.62\n\n\n\n+0.03\n\n\n\n\n\n+1.00%\n\n\n+0.10\n\n\n\n\n\n+2.00%\n"

El resultado es algo más manejable que el xml original, pero requiere más procesamiento.

Usamos la función str_split() con as.data.frame() de R base y tbl_df() de dplyr, para convertir el resultado anterior a un data frame, que llamaremos df_dom.

df_dom <- 
  mtg_dom %>% 
  html_nodes(css = ".index-price-table-paper tbody tr") %>% 
  html_text() %>% 
  str_split(pattern = "\\n", simplify = T) %>%
  as.data.frame() %>%
  tbl_df()

Nuestro resultado es el siguiente.

df_dom
## # A tibble: 277 x 25
##    V1    V2    V3    V4    V5    V6    V7    V8    V9    V10   V11   V12  
##    <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct>
##  1 Tefe~ DOM   Myth~ ""    46.99 ""    ""    ""    -1.89 ""    ""    ""   
##  2 Karn~ DOM   Myth~ ""    31.54 ""    ""    ""    -0.14 ""    ""    ""   
##  3 Lyra~ DOM   Myth~ ""    13.00 ""    ""    ""    +0.01 ""    ""    ""   
##  4 Mox ~ DOM   Myth~ ""    11.42 ""    ""    ""    -0.06 ""    ""    ""   
##  5 Hist~ DOM   Myth~ ""    10.60 ""    ""    ""    -0.03 ""    ""    ""   
##  6 Sulf~ DOM   Rare  ""    4.62  ""    ""    ""    +0.03 ""    ""    ""   
##  7 Jaya~ DOM   Myth~ ""    4.47  ""    ""    ""    0.00  ""    ""    ""   
##  8 Gobl~ DOM   Rare  ""    4.20  ""    ""    ""    -0.02 ""    ""    ""   
##  9 Wood~ DOM   Rare  ""    3.93  ""    ""    ""    +0.07 ""    ""    ""   
## 10 Stee~ DOM   Rare  ""    3.82  ""    ""    ""    -0.10 ""    ""    ""   
## # ... with 267 more rows, and 13 more variables: V13 <fct>, V14 <fct>,
## #   V15 <fct>, V16 <fct>, V17 <fct>, V18 <fct>, V19 <fct>, V20 <fct>,
## #   V21 <fct>, V22 <fct>, V23 <fct>, V24 <fct>, V25 <fct>

Aun tenemos que pulir un poco nuestro data frame, en particular, seleccionando sólo las columnas que nos interesan entre todas las disponibles. Haremos esto con select() de dplyr

df_dom <- 
  df_dom %>% 
  select("Carta" = V1, "Set" = V2, "Rareza" = V3, "Precio" = V5) %>% 
  mutate(Precio = as.numeric(as.character(Precio)), 
         Rareza = factor(Rareza, levels = c("Basic Land","Common",
                                            "Uncommon", "Rare", "Mythic"))) %>% 
  mutate_at(c("Carta", "Set"), as.character) %>% 
  filter(Rareza != "Basic Land")

Nuestro resultado será un data frame con cuatro columnas: el nombre de las cartas, el set, la rareza, y el precio.

df_dom
## # A tibble: 257 x 4
##    Carta                     Set   Rareza Precio
##    <chr>                     <chr> <fct>   <dbl>
##  1 Teferi, Hero of Dominaria DOM   Mythic  47.0 
##  2 Karn, Scion of Urza       DOM   Mythic  31.5 
##  3 Lyra Dawnbringer          DOM   Mythic  13   
##  4 Mox Amber                 DOM   Mythic  11.4 
##  5 History of Benalia        DOM   Mythic  10.6 
##  6 Sulfur Falls              DOM   Rare     4.62
##  7 Jaya Ballard              DOM   Mythic   4.47
##  8 Goblin Chainwhirler       DOM   Rare     4.2 
##  9 Woodland Cemetery         DOM   Rare     3.93
## 10 Steel Leaf Champion       DOM   Rare     3.82
## # ... with 247 more rows

5.1 Definiendo una función para importar y procesar precios

Podemos transformar el proceso anterior en una función, llamada leer_html() para así generar fácilmente data frames a partir del html de páginas de MTG Goldfish.

leer_html <- function(archivo_html) {
  archivo_html %>% 
    read_html() %>% 
    html_nodes(css = ".index-price-table-paper tbody tr") %>% 
    html_text() %>% 
    str_split(pattern = "\\n", simplify = T) %>%
    as.data.frame() %>%
    select("Carta" = V1, "Set" = V2, "Rareza" = V3, "Precio" = V5) %>%
    mutate(Precio = as.numeric(as.character(Precio)),
           Rareza = factor(Rareza, 
                           levels = c("Basic Land","Common", 
                                      "Uncommon", "Rare", "Mythic"))) %>% 
    mutate_at(c("Carta", "Set"), as.character) %>% 
    filter(Rareza != "Basic Land") %>% 
    tbl_df()
}

De este modo, casi hemos terminado el procesamiento de los precios, pero antes tenemos que hacer una recodificación.

5.2 Identificando outliers

Como deseamos analizar las probabilidades de recuperar nuestra inversión en un set de Magic, nos conviene identificar aquellas cartas que tienen un valor excepcionalmente alto, con respecto a las demás. Esta información no permitirá hacer un análisis más fino de los precios con los que contamos.

Por supuesto, estas cartas con precios excepcionales, son outliers. En cuanto a outliers no hay un consenso en cómo caracterizarlos. Para fines de este proyecto, usaremos como criterio para etiquetar un dato como. outlier el usando al generar gráficos de caja y bigote (boxplot), que es:

  • Un dato menor a: el valor del primer cuartil menos una vez y media el rango intercuartílico.
  • Un dato mayor a: el valor del tercer cuartil más una vez y media el rango intercuartílico.

Estos son los datos que se salen de los “bigotes” de un boxplot y son mostrados como puntos en estos diagramas.

Sólo etiquetaremos los outliers “altos”, pues son los relevantes para el análisis que estamos haciendo.

Definimos entonces una función que implemente el criterio anterior. Al darle un vector numérico, nos será devuelto un vector lógico del mismo largo, donde el valor TRUE serán los outliers y FALSE los demás datos.

tag_outlier <- function(datos) {
  ifelse(datos > quantile(datos, .75) + IQR(datos) * 1.5, 
         TRUE, FALSE)
}

Aplicamos esta función, agrupando por nuestras cartas por rareza. No tiene mucho sentido comparar los precios de cartas Comunes con Míticas, pues estas últimas siempre tienen un precio más alto que las primeras por ser más difíciles de obtener.

Hacemos un mutate() adicional para etiquetar los outlier con el nombre de la carta, esto nos será útil más adelante. Podría usar una sola llamada de mutate(), pero he preferido presentarlo así para hacer más claro qué está ocurriendo.

df_dom <- 
  df_dom %>% 
  group_by(Rareza) %>% 
  mutate(Outlier = tag_outlier(Precio)) %>% 
  mutate(Outlier = ifelse(Outlier, Carta, NA))

Nuestro resultado es el siguiente.

df_dom
## # A tibble: 257 x 5
## # Groups:   Rareza [4]
##    Carta                     Set   Rareza Precio Outlier                  
##    <chr>                     <chr> <fct>   <dbl> <chr>                    
##  1 Teferi, Hero of Dominaria DOM   Mythic  47.0  Teferi, Hero of Dominaria
##  2 Karn, Scion of Urza       DOM   Mythic  31.5  Karn, Scion of Urza      
##  3 Lyra Dawnbringer          DOM   Mythic  13    <NA>                     
##  4 Mox Amber                 DOM   Mythic  11.4  <NA>                     
##  5 History of Benalia        DOM   Mythic  10.6  <NA>                     
##  6 Sulfur Falls              DOM   Rare     4.62 Sulfur Falls             
##  7 Jaya Ballard              DOM   Mythic   4.47 <NA>                     
##  8 Goblin Chainwhirler       DOM   Rare     4.2  Goblin Chainwhirler      
##  9 Woodland Cemetery         DOM   Rare     3.93 Woodland Cemetery        
## 10 Steel Leaf Champion       DOM   Rare     3.82 Steel Leaf Champion      
## # ... with 247 more rows

Por supuesto, podemos definir otra función para llevar a cabo el proceso anterior. Llamaremos a esta función etiquetar_outlier().

etiquetar_outlier <- function(mtg_df) {
  mtg_df %>% 
  group_by(Rareza) %>% 
  mutate(Outlier = tag_outlier(Precio)) %>% 
  mutate(Outlier = ifelse(Outlier, Carta, NA))
}

Nuestro siguiente paso es explorar el set “Dominaria”.

6 Análisis exploratorio del set

Iniciemos la exploración con los estadísticos descriptivos más elementales: la media, mediana estándar y máximo y mínimo de los precios.

La desviación estándar, aunque es un estadístico descriptivo por excelencia, en este caso es poco informativa debido a la forma en que se distribuyen los precios. En Magic, los precios no tienen una distribución normal o cercana a una normal, pues tienden a existir muchas cartas con precio bajo y casi idéntico, con algunas pocas cartas con precios muy altos. Por lo tanto, una medida de dispersión como la desviación estándar no nos ayuda mucho a describir lo que nos encontraremos.

Usamos las funciones group_by() y summarize() de dplyr, para aplicar las funciones mean(), median(), min() y max(), que corresponden a los estadísiticos ya mencionados, por rareza.

df_dom %>% 
  group_by(Rareza) %>% 
  summarize(Media = mean(Precio), Mediana = median(Precio), 
            Minimo = min(Precio), Maximo = max(Precio))
## # A tibble: 4 x 5
##   Rareza   Media Mediana Minimo Maximo
##   <fct>    <dbl>   <dbl>  <dbl>  <dbl>
## 1 Common   0.151    0.14   0.11   0.88
## 2 Uncommon 0.309    0.23   0.14   1.73
## 3 Rare     1.03     0.53   0.25   4.62
## 4 Mythic   9.01     2.86   1.13  47.0

Confirmamos que las cartas comunes son las menos caras de todas y las míticas las más costosas. También es evidente que hay una variación muy alta entre el precio mínimo y el máximo, en todas las rarezas, particularmente en raras y míticas.

También podemos visualizar cómo se distribuyen los precios creando un gráfico de densidad con ggplot2, llamando la función geom_density() de este paquete.

df_dom %>% 
  ggplot() +
  aes(Precio) +
  geom_density()

Debido a que tenemos diferencias considerables entre los precios más bajos y más altos, esta gráfica no aporta mucha información.

Sin embargo, ya sabemos que los precios de las cartas dependen de su rareza. Las cartas más raras tienden a ser más caras podemos visualizar los precios por rareza puede ser más útil.

Usamos la función facet_wrap() de ggplot2 para generar un gráfico con las características anteriores. Usamos el argumento scales = "free" para que los ejes x y y sean escaladas de manera independiente. En el eje x se mostrará el precio y en el y la densidad.

df_dom %>% 
  ggplot() +
  aes(Precio, fill = Rareza) +
  geom_density() +
  facet_wrap(~Rareza, scales = "free")

De esta manera es más claro observar la distribución de los precios. También confirmamos lo que anticipábamos, los precios no tienen una distribución parecida a una normal.

Finalmente, podemos explorar los precios de este set por rareza usando diagramas de caja y bigotes (boxplots). La ventaja de emplear esta forma de visualización es que podemos ver fácilemente las cartas que hemos marcado como outliers.

Para lo anterior, usamos la función geom_boxplot() de ggplot2 para generar el diagrama y geom_label_repel() de ggrepel para agregar etiquetas. Esta última función es una versión de geom_label() de ggplot2, que tiene ajustes para evitar que las etiquetas se superpongan, mejorando así la legibilidad.

df_dom %>% 
  ggplot() +
  aes(Rareza, Precio, fill = Rareza) +
  geom_boxplot() +
  geom_label_repel(aes(label = Outlier, color = Rareza), size = 2.5, fill = "white") +
  theme(legend.position = "none")
## Warning: Removed 228 rows containing missing values (geom_label_repel).

Creo que con esto tenemos una buena idea general de los precios de “Dominaria”. En particular, podemos identificar aquellas cartas que son especialmente caras, lo cual es una ayuda para comprar, vender e intercambiar.

Aprovechamos para definir una función que realice todas las operaciones de exploración, a la que llamaremos explorar_set(). De paso, agregamos unas mejoras de presentación a los gráficos, usando la función dollar_format() de scales.

explorar_set <- function(mtg_df) {
  exploracion <- list()
  
  exploracion$precios_resumen <- 
    mtg_df %>% 
    group_by(Rareza) %>% 
    summarize(Media = mean(Precio), Mediana = median(Precio), 
              Minimo = min(Precio), Maximo = max(Precio)) %>% 
    mutate_if(is.numeric, ~round(., 2))
  
  exploracion$precios_rareza <- 
    mtg_df %>% 
    ggplot() +
    aes(Precio, fill = Rareza) +
    geom_density() +
    facet_wrap(~Rareza, scales = "free") +
    scale_x_continuous(labels = dollar_format()) +
    labs(y = "Densidad") +
    theme_minimal() +
    theme(legend.position = "none")
  
  exploracion$boxplot <- 
    mtg_df %>% 
    ggplot() +
    aes(Rareza, Precio, fill = Rareza) +
    geom_boxplot() +
    geom_label_repel(aes(label = Outlier, color = Rareza), size = 2.5, fill = "white") +
    scale_y_continuous(labels = dollar_format()) +
    theme_minimal() +
    theme(legend.position = "none")
  
  exploracion
}

Ahora sí, estamos listos para estimar qué tan probable es que recuperemos nuestra inversión monetaria si decidimos comprar cajas de sobres de una expansión de Magic.

7 Simulaciones

Necesitamos definir dos procesos de simulación, una que simule los resultados de abrir un sobre de cartas y una que simula el resultado de abrir una caja de sobres.

Como mencionamos en la introducción de este proyecto, los sets de Magic, normalmente, son vendidos en sobres que contienen 15 cartas, distribuidas de la misma manera:

Estos sobres, a su vez, generalmente se venden en cajas que contienen 36 de ellos

Por lo tanto, necesitamos simular el contenido de un sobre de cartas y repetir ese ejercicio 36 veces, para así determinar el valor monetario de una caja de Magic.

Hecho esto, podremos entonces comparar el valor monetario de una caja de Magic con la inversión que hagamos para comprarla, es decir, el precio que paguemos por ella.

Con estas consideraciones, comencemos simulando sobres.

7.1 Simulación de un sobre

Crearemos una lista de listas de pares con las rarezas de las cartas y la frecuencia con la que aparecen, tomando como referencia lo mencionado en la sección anterior, de la siguiente manera.

rareza_frecuencia <- list(
  list("Rare", 1),
  list("Uncommon", 3),
  list("Common", 10)
)

Cada lista tiene dos elementos, el primero es la Rareza y el segundo es la frecuencia.

Omitimos las cartas de tierra básica, pues estas generalmente no tienen ningún valor financiero.

Usaremos esta lista con una función anómima dentro de la función map() de purrr. La función map() es muy similar a lapply() de R base, pues aplica una función a todos los elementos de una lista.

Lo que haremos será aplicar una función anónima para filtrar por Rareza las cartas de nuestro data frame con los precios de “Dominaria” (primer elemento) y luego extraer una muestra de ellas igual a la frecuencia con la que aparecen (segundo elemento).

Usaremos entonces, en conjunto con map(), las funciones filter() y sample_n() de dplyr. Haremos esto con set.seed(), para que los resultados sean reproducibles.

set.seed(2018)

map(rareza_frecuencia, function(pareja){
  df_dom %>% 
    filter(Rareza == pareja[[1]]) %>% 
    sample_n(size = pareja[[2]])
})
## [[1]]
## # A tibble: 1 x 5
## # Groups:   Rareza [1]
##   Carta                    Set   Rareza Precio Outlier
##   <chr>                    <chr> <fct>   <dbl> <chr>  
## 1 Traxos, Scourge of Kroog DOM   Rare     0.66 <NA>   
## 
## [[2]]
## # A tibble: 3 x 5
## # Groups:   Rareza [1]
##   Carta                    Set   Rareza   Precio Outlier          
##   <chr>                    <chr> <fct>     <dbl> <chr>            
## 1 Kwende, Pride of Femeref DOM   Uncommon   0.24 <NA>             
## 2 Merfolk Trickster        DOM   Uncommon   0.76 Merfolk Trickster
## 3 Dauntless Bodyguard      DOM   Uncommon   0.35 <NA>             
## 
## [[3]]
## # A tibble: 10 x 5
## # Groups:   Rareza [1]
##    Carta                 Set   Rareza Precio Outlier
##    <chr>                 <chr> <fct>   <dbl> <chr>  
##  1 Thallid Omnivore      DOM   Common   0.14 <NA>   
##  2 Healing Grace         DOM   Common   0.15 <NA>   
##  3 Frenzied Rage         DOM   Common   0.14 <NA>   
##  4 Artificer's Assistant DOM   Common   0.15 <NA>   
##  5 Ghitu Chronicler      DOM   Common   0.13 <NA>   
##  6 Deathbloom Thallid    DOM   Common   0.14 <NA>   
##  7 Adamant Will          DOM   Common   0.14 <NA>   
##  8 Run Amok              DOM   Common   0.14 <NA>   
##  9 Keldon Raider         DOM   Common   0.13 <NA>   
## 10 Seismic Shift         DOM   Common   0.13 <NA>

El resultado es una lista, que representa un sobre de Magic, con una carta rara, tres infrecuentes y trece comunes.

Como usaremos de manera repetida esta función anónima, es mejor que le demos nombre y la definamos. La llamaremos pareja().

pareja <- function(lista, datos) {
  datos %>% 
    filter(Rareza == lista[1]) %>% 
    sample_n(size = as.numeric(lista[2]))
}

De esta manera, podemos usar map() para generar sobres con una sola línea.

set.seed(2018)
map(rareza_frecuencia, pareja, datos = df_dom)
## [[1]]
## # A tibble: 1 x 5
## # Groups:   Rareza [1]
##   Carta                    Set   Rareza Precio Outlier
##   <chr>                    <chr> <fct>   <dbl> <chr>  
## 1 Traxos, Scourge of Kroog DOM   Rare     0.66 <NA>   
## 
## [[2]]
## # A tibble: 3 x 5
## # Groups:   Rareza [1]
##   Carta                    Set   Rareza   Precio Outlier          
##   <chr>                    <chr> <fct>     <dbl> <chr>            
## 1 Kwende, Pride of Femeref DOM   Uncommon   0.24 <NA>             
## 2 Merfolk Trickster        DOM   Uncommon   0.76 Merfolk Trickster
## 3 Dauntless Bodyguard      DOM   Uncommon   0.35 <NA>             
## 
## [[3]]
## # A tibble: 10 x 5
## # Groups:   Rareza [1]
##    Carta                 Set   Rareza Precio Outlier
##    <chr>                 <chr> <fct>   <dbl> <chr>  
##  1 Thallid Omnivore      DOM   Common   0.14 <NA>   
##  2 Healing Grace         DOM   Common   0.15 <NA>   
##  3 Frenzied Rage         DOM   Common   0.14 <NA>   
##  4 Artificer's Assistant DOM   Common   0.15 <NA>   
##  5 Ghitu Chronicler      DOM   Common   0.13 <NA>   
##  6 Deathbloom Thallid    DOM   Common   0.14 <NA>   
##  7 Adamant Will          DOM   Common   0.14 <NA>   
##  8 Run Amok              DOM   Common   0.14 <NA>   
##  9 Keldon Raider         DOM   Common   0.13 <NA>   
## 10 Seismic Shift         DOM   Common   0.13 <NA>

Como necesitamos un data frame para análisis posteriores, corremos lo anterior seguido de la función reduce() de purrr, que aplica una función de manera secuencial a todos los elementos de una lista, por parejas.

Aplicaremos bind_rows() de dplyr para obtener como resultado un data frame.

set.seed(8102)
map(rareza_frecuencia, pareja, datos = df_dom) %>% 
  reduce(bind_rows)
## # A tibble: 14 x 5
## # Groups:   Rareza [3]
##    Carta               Set   Rareza   Precio Outlier
##    <chr>               <chr> <fct>     <dbl> <chr>  
##  1 Squee, the Immortal DOM   Rare       0.62 <NA>   
##  2 Firefist Adept      DOM   Uncommon   0.2  <NA>   
##  3 Urza's Tome         DOM   Uncommon   0.19 <NA>   
##  4 Orcish Vandal       DOM   Uncommon   0.19 <NA>   
##  5 Grow from the Ashes DOM   Common     0.15 <NA>   
##  6 Short Sword         DOM   Common     0.13 <NA>   
##  7 Warlord's Fury      DOM   Common     0.15 <NA>   
##  8 Opt                 DOM   Common     0.25 Opt    
##  9 Cabal Evangel       DOM   Common     0.13 <NA>   
## 10 Timber Gorge        DOM   Common     0.11 <NA>   
## 11 Charge              DOM   Common     0.14 <NA>   
## 12 Gideon's Reproach   DOM   Common     0.15 <NA>   
## 13 Llanowar Envoy      DOM   Common     0.14 <NA>   
## 14 Krosan Druid        DOM   Common     0.13 <NA>

Lo anterior, aunque cumple nuestro cometido, no nos permite crear sobres con cartas míticas. Necesitamos solucionar esta situación.

7.2 Las cartas míticas

Dado que las cartas míticas aparecen en un sobre una de cada ocho veces (\(p = \frac{1}{8}\)), podemos simular este comportamiento con una distribución binomial.

Usamos la función rbinom(), para generar 1 y 0 al azar, teniendo el 1 una probabilidad de \(\frac{1}{8}\) de ocurrir, esto es, 12.5% de los casos.

Pongamos esto a prueba, simulando 40 números con estas probabilidades.

rbinom(n = 40, size = 1, prob = 1/8)
##  [1] 0 0 1 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0
## [36] 0 0 0 0 0

Luce bien.

Lo que haremos será establecer una condición con ìf. Si la rareza de la que estamos extrayendo cartas es “Rare” y al mismo tiempo obtenemos un 1 de simular una distribución binomial con las características descritas arriba.

Dado lo anterior, lo siguiente nos devolverá una carta Rara.

set.seed(6)
rareza_frecuencia %>% 
  map(function(x) {
    if(x[[1]] == "Rare" & rbinom(n = 1, size = 1, prob = 1/8)) {
      "Mythic"
    } else {
      x[[1]]
    }
  })
## [[1]]
## [1] "Rare"
## 
## [[2]]
## [1] "Uncommon"
## 
## [[3]]
## [1] "Common"

Y lo siguiente una carta Mítica.

set.seed(7)
rareza_frecuencia %>% 
  map(function(x) {
    if(x[[1]] == "Rare" & rbinom(n = 1, size = 1, prob = 1/8)) {
      "Mythic"
    } else {
      x[[1]]
    }
  })
## [[1]]
## [1] "Mythic"
## 
## [[2]]
## [1] "Uncommon"
## 
## [[3]]
## [1] "Common"

Combinamos esto con la función pareja() para definir una función llamada simular_sobre().

simular_sobre <- function(tabla) {
  rareza_frecuencia <- list(
    c("Rare", 1),
    c("Uncommon", 3),
    c("Common", 10)
  )
  
  rareza_frecuencia <- 
    map(rareza_frecuencia, function(x) {
      if(x[[1]] == "Rare" & rbinom(n = 1, size = 1, prob = 1/8)) {
        x[[1]] <- "Mythic"
      } else {
        x[[1]]
      }
      x
    })
  
  map(rareza_frecuencia, pareja, datos = tabla) %>% 
    reduce(bind_rows)
}

Pongamos a prueba nuestra función simular_sobre().

set.seed(7)
simular_sobre(tabla = df_dom)
## # A tibble: 14 x 5
## # Groups:   Rareza [3]
##    Carta                 Set   Rareza   Precio Outlier            
##    <chr>                 <chr> <fct>     <dbl> <chr>              
##  1 Karn, Scion of Urza   DOM   Mythic    31.5  Karn, Scion of Urza
##  2 Fight with Fire       DOM   Uncommon   0.3  <NA>               
##  3 Amaranthine Wall      DOM   Uncommon   0.2  <NA>               
##  4 Settle the Score      DOM   Uncommon   0.25 <NA>               
##  5 Blessed Light         DOM   Common     0.13 <NA>               
##  6 Temporal Machinations DOM   Common     0.15 <NA>               
##  7 Knight of New Benalia DOM   Common     0.14 <NA>               
##  8 Fiery Intervention    DOM   Common     0.12 <NA>               
##  9 Sparring Construct    DOM   Common     0.15 <NA>               
## 10 Mesa Unicorn          DOM   Common     0.14 <NA>               
## 11 Corrosive Ooze        DOM   Common     0.15 <NA>               
## 12 Pierce the Sky        DOM   Common     0.14 <NA>               
## 13 Deep Freeze           DOM   Common     0.15 <NA>               
## 14 Blessing of Belzenlok DOM   Common     0.14 <NA>

Equipados con esta función, podremos crear cajas de sobres fácilmente.

7.3 Simular caja de sobres

Una vez más, usamos la función map(), aplicando la función simular_sobre() 36 veces.

set.seed(8244)
caja_dom <- map(1:36, ~simular_sobre(tabla = df_dom))

De esta manera obtenemos una lista con 36 elementos, cada uno de ellos representando un sobre de Magic. Por ejemplo, este es el cuarto “sobre” de la lista anterior.

caja_dom[[4]]
## # A tibble: 14 x 5
## # Groups:   Rareza [3]
##    Carta                       Set   Rareza   Precio Outlier            
##    <chr>                       <chr> <fct>     <dbl> <chr>              
##  1 Primevals' Glorious Rebirth DOM   Rare       0.39 <NA>               
##  2 Goblin Barrage              DOM   Uncommon   0.21 <NA>               
##  3 Teferi's Sentinel           DOM   Uncommon   0.2  <NA>               
##  4 Final Parting               DOM   Uncommon   0.25 <NA>               
##  5 Deathbloom Thallid          DOM   Common     0.14 <NA>               
##  6 Dub                         DOM   Common     0.14 <NA>               
##  7 Gaea's Protector            DOM   Common     0.14 <NA>               
##  8 Rampaging Cyclops           DOM   Common     0.14 <NA>               
##  9 Radiating Lightning         DOM   Common     0.14 <NA>               
## 10 Cabal Paladin               DOM   Common     0.14 <NA>               
## 11 Adventurous Impulse         DOM   Common     0.18 Adventurous Impulse
## 12 Arbor Armament              DOM   Common     0.14 <NA>               
## 13 Seismic Shift               DOM   Common     0.13 <NA>               
## 14 Sergeant-at-Arms            DOM   Common     0.15 <NA>

Definamos entonces una función para simular una caja de Magic, que nos de como resultado un data frame con los 36 sobres, utilizando reduce() y bind_rows().

simular_caja <- function(datos) {
  map(1:36, ~simular_sobre(tabla = df_dom)) %>% 
    reduce(bind_rows)
}

Probamos nuestra función y asignemos el resultado al objeto caja_dom.

set.seed(8244)
caja_dom <- simular_caja(datos = df_fom)

Comprobamos que hemos generado 36 cartas Raras o Míticas.

caja_dom %>% 
  filter(Rareza %in% c("Rare", "Mythic"))
## # A tibble: 36 x 5
## # Groups:   Rareza [2]
##    Carta                       Set   Rareza Precio Outlier                
##    <chr>                       <chr> <fct>   <dbl> <chr>                  
##  1 Oath of Teferi              DOM   Rare     0.59 <NA>                   
##  2 Jodah, Archmage Eternal     DOM   Rare     0.46 <NA>                   
##  3 Naban, Dean of Iteration    DOM   Rare     0.59 <NA>                   
##  4 Primevals' Glorious Rebirth DOM   Rare     0.39 <NA>                   
##  5 Shalai, Voice of Plenty     DOM   Rare     2.84 Shalai, Voice of Plenty
##  6 Squee, the Immortal         DOM   Rare     0.62 <NA>                   
##  7 The Mending of Dominaria    DOM   Rare     0.49 <NA>                   
##  8 Cabal Stronghold            DOM   Rare     1    <NA>                   
##  9 Karn, Scion of Urza         DOM   Mythic  31.5  Karn, Scion of Urza    
## 10 Primevals' Glorious Rebirth DOM   Rare     0.39 <NA>                   
## # ... with 26 more rows

Para una simulación apropiada, necesitamos repetir generar muchas cajas de sobres.

7.4 Simulación de múltiples cajas

Usamos map() para repetir 100 veces la función simular_caja() y así obtener esa misma cantidad de cajas. Una vez más, empleamos reduce() para obtener como resultado un data frame.

set.seed(3356)

ciencajas_dom <- 
  map(1:100, function(x) {
    simular_caja(datos = df_dom) %>% 
      summarize(Valor = sum(Precio))
  }) %>% 
  reduce(bind_rows)

Como anteriormente dejamos agrupados nuestros datos por Rareza, obtendremos un data frame con cerca de 400 renglones, uno por rareza de cada caja de Magic simulada. No siempre serán 400 renglones, pues hay una probabilidad pequeña de que alguna desafortunada caja no tenga ni una sola carta Mítica y con ello su valor se reduzca.

ciencajas_dom
## # A tibble: 398 x 2
##    Rareza   Valor
##    <fct>    <dbl>
##  1 Common    53.0
##  2 Uncommon  35.7
##  3 Rare      29.1
##  4 Mythic    48.4
##  5 Common    54.5
##  6 Uncommon  34.4
##  7 Rare      28.6
##  8 Mythic    86.4
##  9 Common    52.7
## 10 Uncommon  36.4
## # ... with 388 more rows

Ahora podemos calcular el valor promedio de una caja de “Dominaria” y de las cartas por rareza.

7.5 Valor promedio de una caja y por Rareza

Si los deseamos, podemos obtener fácilmente el valor promedio por rareza de las cartas simuladas usando summarize() de dplyr.

ciencajas_dom %>% 
  group_by(Rareza) %>% 
  summarize(Promedio = mean(Valor))
## # A tibble: 4 x 2
##   Rareza   Promedio
##   <fct>       <dbl>
## 1 Common       54.3
## 2 Uncommon     33.5
## 3 Rare         31.8
## 4 Mythic       42.0

Sin embargo, necesitamos hacer un pequeño ajuste si buscamos conocer el valor promedio de cada caja de sobres simulada. Necesitamos proporcionar un identificador por caja, para así poder obtener un valor total por cada una de ellas y, con este, calcular el valor promedio por caja.

Hagamos este ajuste y de una vez definamos una función llamada simular_ciencajas(), aunque en realidad, podremos definir el número de iteraciones que deseemos, no sólo 100.

simular_ciencajas <- function(datos, iteraciones = 100) {
  map(1:iteraciones, function(num_caja) {
    simular_caja(datos = datos) %>% 
      summarize(Valor = sum(Precio)) %>% 
      mutate(Id_caja = num_caja)
  }) %>% 
  reduce(bind_rows)
}

Pongamos a prueba nuestra función.

set.seed(3356)

ciencajas_dom <- simular_ciencajas(datos = df_dom)

Nuestro resultado es muy similar al anterior, sólo que ahora tenemos identificador por caja.

ciencajas_dom
## # A tibble: 398 x 3
##    Rareza   Valor Id_caja
##    <fct>    <dbl>   <int>
##  1 Common    53.0       1
##  2 Uncommon  35.7       1
##  3 Rare      29.1       1
##  4 Mythic    48.4       1
##  5 Common    54.5       2
##  6 Uncommon  34.4       2
##  7 Rare      28.6       2
##  8 Mythic    86.4       2
##  9 Common    52.7       3
## 10 Uncommon  36.4       3
## # ... with 388 more rows

Con este identificador podemos obtener el valor promedio de una caja de sobres de “Dominaria”.

ciencajas_dom %>% 
  group_by(Id_caja) %>% 
  summarize(Suma = sum(Valor)) %>% 
  summarize(Media = mean(Suma))
## # A tibble: 1 x 1
##   Media
##   <dbl>
## 1  161.

La cosa marcha bien, pero podemos hacer nuestro análisis más fino si consideramos una característica del valor de una caja de Magic: las cartas comunes generalmente tiene poca “liquidez”.

Aunque las cartas comunes tienen un valor monetario, a menos que sea una carta de alta demanda y por tanto de un precio particularmente alto, es un poco difícil venderlas y recuperar lo invertido en obtenerlas.

Por suerte nosotros ya hemos etiquetado las cartas comunes con precios inusualmente altos con nuestra función tag_outlier(). De este modo, sólo incluiremos a estas al calcular el valor promedio de una caja.

Agregamos esta información en la definición de la función simular_ciencajas().

simular_ciencajas <- function(datos, iteraciones = 100) {
  map(1:iteraciones, function(num_caja) {
    simular_caja(datos = datos) %>% 
      filter(Rareza != "Common" | (Rareza == "Common" & !is.na(Outlier)) ) %>% 
      summarize(Valor = sum(Precio)) %>% 
      mutate(Id_caja = num_caja)
  }) %>% 
  reduce(bind_rows)
}

Con esta nueva versión de nuestra función simular_ciencajas(), simulamos cien cajas de “Dominaria” y, con esos datos, obtenemos su valor promedio. De una vez calculemos también la mediana, el valor que tiene 50% de los precios debajo de él y 50% por encima.

set.seed(3356)
ciencajas_dom <- simular_ciencajas(datos = df_dom)

ciencajas_dom
## # A tibble: 398 x 3
##    Rareza   Valor Id_caja
##    <fct>    <dbl>   <int>
##  1 Common    6.42       1
##  2 Uncommon 35.7        1
##  3 Rare     29.1        1
##  4 Mythic   48.4        1
##  5 Common    7.1        2
##  6 Uncommon 34.4        2
##  7 Rare     28.6        2
##  8 Mythic   86.4        2
##  9 Common    5.04       3
## 10 Uncommon 36.4        3
## # ... with 388 more rows
ciencajas_dom %>% 
  group_by(Id_caja) %>% 
  summarize(Suma = sum(Valor)) %>% 
  summarize(Media = mean(Suma), Mediana = median(Suma))
## # A tibble: 1 x 2
##   Media Mediana
##   <dbl>   <dbl>
## 1  114.    106.

Nuestro promedio, naturalmente, es menor que el obtuvimos antes de excluir las cartas comunes.

Con lo que hemos hecho hasta ahora, podríamos decir cuál es el valor promedio que esperaríamos obtener de una caja de sobres de “Dominaria”. Con ello podemos hacernos una idea general sobre la posibilidad de recuperar nuestra inversión. Si compramos una caja de “Dominaria” por un precio inferior al valor promedio esperado de ellas, deberíamos recuperar nuestra inversión ¿cierto?

Bueno, la cosa no es tan sencilla. Calculemos, por fin, la probabilidad de recuperar nuestra inversión.

8 Probabilidad de recuperar inversión

Después de simular el valor de múltiples cajas de sobres de Magic, podemos calcular la probabilidad de recuperar lo que hemos invertido en una de ellas a partir de una función de densidad.

Primero, obtenemos el valor de cada caja, agrupando los Precios de las cartas en ellas por Id_caja. Una vez que hemos obtenido estos valores, los extraemos como un vector utilizando con la función pull() de dplyr.

valorcajas_dom <- 
  ciencajas_dom %>% 
  group_by(Id_caja) %>% 
  summarize(Suma = sum(Valor)) %>% 
  pull(Suma)

Este es nuestro resultado.

valorcajas_dom
##   [1] 119.57 156.47 135.87 129.77 145.01  92.61  83.36  81.80  83.36  94.87
##  [11] 116.89  81.52 146.44  89.45 124.25 155.24  97.28 140.66  83.01 144.91
##  [21] 137.17 166.77 124.05  75.35  77.33  80.35  84.65 153.38  94.10 117.94
##  [31] 114.88  80.99 144.39 109.78  98.81 100.64 158.83  96.26  88.66 139.78
##  [41] 155.45  88.49  75.97 150.07 105.70  87.75 176.57  81.47 116.65 123.54
##  [51]  93.45 131.73 136.54 115.67  87.20 223.06  86.77 106.12  76.46 144.24
##  [61] 104.59 102.96 121.24  87.94 117.09  84.11 130.13 101.38 161.11  87.63
##  [71] 108.55 151.34  95.74 123.01 182.26 104.25  94.69 104.37 133.66 139.48
##  [81] 140.47  70.70  97.20  74.48  72.47  94.24  88.38 160.94  91.93  88.48
##  [91] 111.76 149.46 137.43  82.05  78.22 195.35  82.63 134.74 126.79  82.04

Definimos una función que realice lo anterior, llamada valor_cajas.

valor_cajas <- function(cajas_simuladas) {
  cajas_simuladas %>% 
    group_by(Id_caja) %>% 
    summarize(Suma = sum(Valor)) %>% 
    pull(Suma)
}

Usamos density() en nuestro vector anterior para obtener la función de densidad de estos valores.

densidad_dom <- density(valorcajas_dom)

Obtenemos lo siguiente.

densidad_dom
## 
## Call:
##  density.default(x = valorcajas_dom)
## 
## Data: valorcajas_dom (100 obs.); Bandwidth 'bw' = 11.16
## 
##        x                y            
##  Min.   : 37.22   Min.   :4.020e-06  
##  1st Qu.: 92.05   1st Qu.:3.674e-04  
##  Median :146.88   Median :2.409e-03  
##  Mean   :146.88   Mean   :4.555e-03  
##  3rd Qu.:201.71   3rd Qu.:8.499e-03  
##  Max.   :256.54   Max.   :1.396e-02

Si deseamos obtener una gráfica de esta función, usamos ggplot() y geom_area() de ggplot2. Pero antes necesitamos extraer la información de los ejes x y y, para después convertirla a un data frame con tbl_df().

densidad_dom[c("x", "y")] %>% 
  tbl_df() %>%
  ggplot() +
  aes(x, y) +
  geom_area()

Con esto obtenemos una curva de función y su respectiva área debajo de ella. Esta área representa, aproximadamente, una distribución de todos los valores que podrían tomar las cajas de sobres Magic, a partir de la simulación que hemos realizado. Es decir, el 100% de nuestros casos.

Podemos trazar una línea vertical que divida esta área bajo la curva en dos, justo en el valor de nuestra mediana: 104. Si hacemos esto, tendremos dos segmentos del área, uno con 50% de los valores por debajo de 104 (área a la izquierda) y 50% con los valores por encima de este valor (área a la derecha).

Como esta es una representación de una distribución, lo anterior quiere decir que si sacamos un valor al azar de esta distribución, tenemos 50% de probabilidad de que sea menor a 104 y 50% de probabilidad que sea mayor.

Veamos como luce lo anterior con un valor de 105, que obtuvimos antes.

densidad_dom[c("x", "y")] %>% 
  tbl_df() %>%
  ggplot() +
  aes(x, y) +
  geom_area() +
  geom_vline(xintercept = 105, color = "red")

¡Interesante!

Por lo tanto, si queremos calcular la probabilidad de que recuperemos nuestra inversión al comprar una caja de sobres de Magic, debemos calcular el área que resulta de segmentar nuestra distribución en dos.

Para calcular el área de un segmento bajo la curva, usamos las funciones approxfun() e integrate().

Como su nombre lo indica, la función integrate() usará integración para calcular un área, por lo que nos pedirá un límite inferior y uno superior.

Supongamos que hemos comprado una caja de sobres de “Dominaria” en 100 dólares. Usaremos este valor como límite inferior y el valor máximo de nuestra distribución como límite superior.

densidad_dom %>% 
  approxfun() %>% 
  integrate(upper = max(valorcajas_dom), lower = 100)
## 0.5875856 with absolute error < 3e-05

¡Perfecto!

Lo que hemos obtenido es la probabilidad de que recuperemos nuestra inversión, esto es decir, hay 58% de probabilidad de comprar una caja y que esta tenga un valor mayor que 100 USD.

En este caso es más o menos lanzar una moneda al aire recuperar nuestra inversión si pagamos 100 USD por una caja de “Dominaria”.

Transformemos el proceso anterior a funciones, para repetirlo fácilmente.

9 Sistematización del análisis

Primero, definimos una función que calcule la función de densidad, la probabilidad a partir de un costo que elijamos, y el data frame de la función de densidad.

Además, aprovecharemos para etiquetar los valor en el data frame de la función de densidad como menores o mayores al costo elegido.

magic_densidad <- function(valor_cajas, costo_pagado) {
  magic <- list()
  
  magic$costo_pagado <- costo_pagado
  
  magic$densidad <- 
    density(valor_cajas)
  
  magic$probabilidad <- 
    magic$densidad %>% 
    approxfun() %>% 
    integrate(upper = max(valor_cajas), lower = costo_pagado)
  
  magic$df_densidad <- 
    magic$densidad[c("x", "y")] %>% 
    tbl_df() %>% 
    mutate(Tipo = ifelse(x < costo_pagado, "Menor", "Mayor"))
  
  magic
}

El resultado será una lista con cuatro elementos: el costo pagado, la función de densidad, probabilidad y un data frame.

magic_densidad(valorcajas_dom, costo_pagado = 100)
## $costo_pagado
## [1] 100
## 
## $densidad
## 
## Call:
##  density.default(x = valor_cajas)
## 
## Data: valor_cajas (100 obs.);    Bandwidth 'bw' = 11.16
## 
##        x                y            
##  Min.   : 37.22   Min.   :4.020e-06  
##  1st Qu.: 92.05   1st Qu.:3.674e-04  
##  Median :146.88   Median :2.409e-03  
##  Mean   :146.88   Mean   :4.555e-03  
##  3rd Qu.:201.71   3rd Qu.:8.499e-03  
##  Max.   :256.54   Max.   :1.396e-02  
## 
## $probabilidad
## 0.5875856 with absolute error < 3e-05
## 
## $df_densidad
## # A tibble: 512 x 3
##        x         y Tipo 
##    <dbl>     <dbl> <chr>
##  1  37.2 0.0000131 Menor
##  2  37.6 0.0000149 Menor
##  3  38.1 0.0000169 Menor
##  4  38.5 0.0000191 Menor
##  5  38.9 0.0000216 Menor
##  6  39.4 0.0000245 Menor
##  7  39.8 0.0000276 Menor
##  8  40.2 0.0000310 Menor
##  9  40.7 0.0000350 Menor
## 10  41.1 0.0000393 Menor
## # ... with 502 more rows

Ahora, creamos una función que use esta lista para generar un gráfico.

plot_densidad <- function(lista_densidad) {
  label_costo <- 
    paste0("Costo pagado: ", lista_densidad$costo_pagado, " USD")
  
  label_prob <- 
    paste0("Probabilidad de recuperar inversión: ", 
           round(lista_densidad$probabilidad$value, 4) * 100, 
           "%")
  
  lista_densidad$df_densidad %>% 
    ggplot() +
    aes(x, y, fill = Tipo) +
    geom_area() +
    labs(title =  paste0(label_costo, "\n", label_prob)) +
    scale_y_continuous(expand = c(0, 0)) +
    scale_x_continuous(labels = dollar_format()) +
    labs(x = "USD", y = "Densidad") +
    theme_minimal() +
    theme(legend.position = "top")
}

Hecho esto, podemos probar con otros costos de una caja de Dominaria. Por ejemplo, si pagamos 90 USD, naturalmente nos irá mejor.

 magic_densidad(valorcajas_dom, 90) %>% 
   plot_densidad()

Vamos a integrar todos los pasos de nuestro análisis anterior en una sola función llamada analisis_set().

analisis_set <- function(set_html, costo_pagado = 100) {
  analisis <- list()
  
  analisis$df <- leer_html(set_html) %>% etiquetar_outlier()
  
  analisis$explorar <- explorar_set(analisis$df)
  
  analisis$simulacion <- simular_ciencajas(analisis$df)
  
  analisis$valor <- valor_cajas(analisis$simulacion)
  
  analisis$densidad <- magic_densidad(analisis$valor, costo_pagado = costo_pagado)
  
  analisis$inversion <- plot_densidad(analisis$densidad)
  
  analisis
}

De esta manera, podemos realizar el análisis de cualquier set en un solo paso, siempre y cuando descarguemos primero los datos de este de MTG Goldfish. El resultado será una lista con el data frame con los precios del set, el análisis exploratorio, incluidas gráficas, el resultado de las simulaciones, y el análisis de la probabilidad de recuperar información.

Probemos descargando la información del set “Ixalan”, que tiene la clave “IXL” con nuestra función descargar_set()

descargar_set(clave = "XLN")

Con los datos de “Ixalan”, podemos realizar una simulación usando nuestra función analisis_set() que nos permitirá calcular la probabilidad de recuperar nuestra inversión si compramos cajas de este set por 90 USD cada una.

set.seed(25)
ixalan_lista <- analisis_set("goldfish_XLN.html", costo_pagado = 90)

Veamos nuestros resultados.

ixalan_lista
## $df
## # A tibble: 267 x 5
## # Groups:   Rareza [4]
##    Carta                    Set   Rareza Precio Outlier                 
##    <chr>                    <chr> <fct>   <dbl> <chr>                   
##  1 Search for Azcanta       XLN   Rare    21.0  Search for Azcanta      
##  2 Carnage Tyrant           XLN   Mythic  18.9  Carnage Tyrant          
##  3 Vraska's Contempt        XLN   Rare    14.5  Vraska's Contempt       
##  4 Vraska, Relic Seeker     XLN   Mythic   8.77 Vraska, Relic Seeker    
##  5 Settle the Wreckage      XLN   Rare     7.42 Settle the Wreckage     
##  6 Growing Rites of Itlimoc XLN   Rare     6    Growing Rites of Itlimoc
##  7 Glacial Fortress         XLN   Rare     4.99 <NA>                    
##  8 Drowned Catacomb         XLN   Rare     4.95 <NA>                    
##  9 Treasure Map             XLN   Rare     4.24 <NA>                    
## 10 Sunpetal Grove           XLN   Rare     4    <NA>                    
## # ... with 257 more rows
## 
## $explorar
## $explorar$precios_resumen
## # A tibble: 4 x 5
##   Rareza   Media Mediana Minimo Maximo
##   <fct>    <dbl>   <dbl>  <dbl>  <dbl>
## 1 Common    0.15    0.14   0.12   0.31
## 2 Uncommon  0.37    0.22   0.19   3.5 
## 3 Rare      2.03    0.75   0.32  21.0 
## 4 Mythic    3.42    1.75   0.72  18.9 
## 
## $explorar$precios_rareza

## 
## $explorar$boxplot
## Warning: Removed 244 rows containing missing values (geom_label_repel).

## 
## 
## $simulacion
## # A tibble: 397 x 3
##    Rareza   Valor Id_caja
##    <fct>    <dbl>   <int>
##  1 Common    5.16       1
##  2 Uncommon 37.3        1
##  3 Rare     35.2        1
##  4 Mythic   97.3        1
##  5 Common    7.08       2
##  6 Uncommon 33.3        2
##  7 Rare     29.3        2
##  8 Mythic   54.7        2
##  9 Common    6.29       3
## 10 Uncommon 33.0        3
## # ... with 387 more rows
## 
## $valor
##   [1] 174.90 124.32  90.38  65.45  90.07  76.19 164.54 114.02  69.08 170.50
##  [11] 150.64 112.79 137.96  99.70 159.40 130.86  98.39 154.86 119.92 216.09
##  [21] 111.08  79.53 129.72 125.33 147.36 133.00  86.57  77.84 131.81  85.03
##  [31] 170.15 137.96 171.46  96.32 124.70 180.97  83.42 133.85  95.41  94.55
##  [41]  93.14  97.12 109.09 110.51 111.38  98.32  95.89 127.21  89.81 150.29
##  [51] 100.50  89.55  76.39 128.75  82.65  97.03  72.37 132.23 108.64  73.42
##  [61]  90.52 133.59  80.30  93.76 148.57  80.40 159.60 164.89 145.37  93.74
##  [71] 193.68  92.32 119.95  90.02 130.82 165.24  81.66 103.21  95.23 115.61
##  [81]  78.99 164.18 123.06 108.66 202.00 190.49  73.20  83.96 116.63  71.87
##  [91] 147.48  85.19  84.68  78.20  89.46  88.15 118.36  71.03 118.95 112.71
## 
## $densidad
## $densidad$costo_pagado
## [1] 90
## 
## $densidad$densidad
## 
## Call:
##  density.default(x = valor_cajas)
## 
## Data: valor_cajas (100 obs.);    Bandwidth 'bw' = 11.8
## 
##        x                y            
##  Min.   : 30.05   Min.   :3.864e-06  
##  1st Qu.: 85.41   1st Qu.:6.273e-04  
##  Median :140.77   Median :3.563e-03  
##  Mean   :140.77   Mean   :4.511e-03  
##  3rd Qu.:196.13   3rd Qu.:8.196e-03  
##  Max.   :251.49   Max.   :1.278e-02  
## 
## $densidad$probabilidad
## 0.7141344 with absolute error < 6e-05
## 
## $densidad$df_densidad
## # A tibble: 512 x 3
##        x          y Tipo 
##    <dbl>      <dbl> <chr>
##  1  30.1 0.00000891 Menor
##  2  30.5 0.0000100  Menor
##  3  30.9 0.0000113  Menor
##  4  31.4 0.0000128  Menor
##  5  31.8 0.0000144  Menor
##  6  32.2 0.0000161  Menor
##  7  32.7 0.0000181  Menor
##  8  33.1 0.0000203  Menor
##  9  33.5 0.0000227  Menor
## 10  34.0 0.0000255  Menor
## # ... with 502 more rows
## 
## 
## $inversion

¡Vaya! no hay mucha diferencia entre estos sets, así que cualquiera de los dos tendrá las mismas probabilidades de devolver nuestra inversión.

10 Conclusiones

En este artículo revisamos las diferentes etapas de un pequeño proyecto de Data Science en el que hemos calculado la probabilidad de recuperar nuestra inversión al comprar cajas de sobres de Magic: the Gathering, a partir de la información de MTG Goldfish

Para esta tarea hemos usados los paquetes xml2 y rvest para obtener y procesar información de un sitio web, así como la familia tidyverse con ggrepel y scales para analizar y visualizar resultados.

Creo que en términos generales hemos creado una herramienta que nos puede ayudar a tomar decisiones a la hora de comprar decisiones al gastar dinero en Magic, o al menos, entender mejor las tendencias financieras relacionadas con este juego de cartas coleccionable.

Desde luego, aún hay formas en las que podemos perfeccionar este proyecto, por ejemplo:

Pero algunas de estas cosas, pueden ser motivo de futuros proyectos.


Consultas, dudas, propuestas de temas, comentarios y correcciones son bienvenidas:

El código y los datos usados en este documento se encuentran en Github: