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.
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.
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.
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.
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.
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&c2=60 ...
## [16] <script type="text/javascript">\n(function () {\n var d = new Ima ...
## [17] <noscript>\n <div><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.
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
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.
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:
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”.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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: