Ir al contenido principal

Warriors-Rockets: Efectividad en lanzamientos de 3 puntos

Es sabido, que desde hace algunos años, la importancia de los lanzamientos de 3 puntos, ha venido incrementandose. Podría decirse que todos los equipos han experimentado una mayor inclinación a favorecer este tipo de jugada, entre los más recurrentes cuando se habla de este aspecto, se encuentran Rockets y Warriors que resaltan por contar con diestros lanzadores de 3 puntos, como por ejemplo Harden por Houston o los splash brothers por Golden State.
Frecuentemente, en conversaciones con amigos, surge el tema (a veces discusión) sobre cuál equipo ostenta mayor efectividad en este tipo de lanzamientos. A pesar de toda la información existente a ese respecto, y seguramente existirá un dictamen definitivo y oficial sobre el particular; en una tarde de ocio, facilmente puede emerger la idea de hacer un ejercicio para intentar comprobar, si existe alguna ventaja de un equipo sobre el otro; 'cometí' ese ejercicio recientemente y lo presento a continuación : - ]]

Fue necesario disponer de algunos datos, claro esta: recordé la excelente página de Basketball Reference, dónde presentan información sobre todos los equipos de la NBA. Por simplificar y por tratarse de un simple ejercicio, decidí usar como muestra, el match, disputado entre abril y mayo, entre Golden State y Houston, durante los juegos de playoff de la NBA y con esos datos hacer un contraste de proporciones, sobre la efectividad de lanzamientos de 3 puntos de cada equipo.
En las direcciones listadas abajo, se encuentran tablas con descripciones de las incidencias, jugada por jugada de los partidos del playoff.
library(rvest)
library(tidyverse)
library(magrittr)
library(infer)

url01 <- "https://www.basketball-reference.com/boxscores/pbp/201904280GSW.html"
url02 <- "https://www.basketball-reference.com/boxscores/pbp/201904300GSW.html"
url03 <- "https://www.basketball-reference.com/boxscores/pbp/201905040HOU.html"
url04 <- "https://www.basketball-reference.com/boxscores/pbp/201905060HOU.html"
url05 <- "https://www.basketball-reference.com/boxscores/pbp/201905080GSW.html"
url06 <- "https://www.basketball-reference.com/boxscores/pbp/201905100HOU.html"


Guardamos las direcciones web, en vectores tipo character, y posteriormente esos vectores los incluímos en una lista 'urlist'
urlist <- list(url01, url03, url04, url05, url06)


Mediante el funcional lapply y se aplica la función read_html del paquete rvest con el que se capta la información contenida en los sites mencionados arriba.
htmlist <- lapply(urlist, function(x) read_html(x))

Para extraer las tablas que presentan los datos de las jugadas, escribimos una función y luego la pasamos a lapply para asi tener una lista con seis tablas, cada una correspondiente a la serie de seis juegos playoff entre Warriors y Rockets
html2table <- function(x){
  x %>% 
    html_nodes("table") %>% 
    html_table()
}

tb <- lapply(htmlist, function(x) html2table(x))

Habíamos creado una lista resultante de la lectura hecha por el comando read_html en las direcciones web, posteriormente se extrajeron las tablas, contentivas de los datos, con la función html2table. Asi 'tb' será una lista de seis tablas. De seguidas, será bueno identificar los datos que corresponden a cada juego.
v <- c(1, 2, 3, 4, 5, 6)

En R, suelen prevenir contra el uso de for-loops sin embargo, en este contexto, resulta intuitivo y no afectará ninguna rapidez, dado que la cantidad de datos es pequeña. Simplemente se trata de crear un variable para cada una de las tablas.
for (i in seq_along(tb)) {
  
  tb[[i]][["nro"]] <- v[i] 
  
}

El orden de algunas columnas cambia, dependiendo de la circunstacia si el equipo es visitante
vec <- lapply(seq_along(tb), 
              function(i) tb[[i]][which(row.names(tb[[i]]) == 1) , ])

vec[2:3] %>% knitr::kable()

|X1st.Q |X1st.Q.1 |X1st.Q.2 |X1st.Q.3 |X1st.Q.4 |X1st.Q.5     | nro|
|:------|:--------|:--------|:--------|:--------|:------------|---:|
|Time   |Houston  |         |Score    |         |Golden State |   2|

|X1st.Q |X1st.Q.1     |X1st.Q.2 |X1st.Q.3 |X1st.Q.4 |X1st.Q.5 | nro|
|:------|:------------|:--------|:--------|:--------|:--------|---:|
|Time   |Golden State |         |Score    |         |Houston  |   3|

Entonces, según sea el orden de las columnas usaremos uno de estos dos vectores:
cols01 <- c("tiempo", "Inc_houst",
            "anot_ho", "score",
            "anot_gs", "Inc_gs", "nro")

cols02 <- c("tiempo", "Inc_gs",
            "anot_gs", 
            "score",
            "anot_ho",
            "Inc_houst", "nro")

Luego mediante otro for-loop:
for (i in seq_along(tb)) {
  
  
  if (tb[[i]][which(row.names(tb[[i]]) == 1) , ][2] == "Houston"){
    
    colnames(tb[[i]])<- cols01
    
  } else{
    
    colnames(tb[[i]]) <- cols02
  }
  
}

Asignamos las columnas en el orden conveniente, según los equpos sean visitantes o no. Para tener una idea de los porcentajes de lanzamientos de tres:
fundf02 <- function(x) {
  
  ho <- x %>% filter(str_detect(Inc_houst, "3-pt")) %>% 
    mutate(int_3p = 1,  
           conv_3p = ifelse(str_detect(anot_ho, "\\+3"), 1, 0),
           t = "ho") %>% 
    select(int_3p, conv_3p, t, nro)
  
  gs <- x %>% filter(str_detect(Inc_gs, "3-pt")) %>% 
    mutate(int_3p = 1, 
           conv_3p = ifelse(str_detect(anot_gs, "\\+3"), 1, 0),
           t = "gs") %>% 
    select(int_3p, conv_3p, t, nro)
  
  return(data.frame(rbind(ho, gs)))
  
}

El comando del paquete stringr::str_detect(anot_ho, "\\+3") capta los intentos, fallidos o no, de lanzamientos de 3 por parte de Houston. stringr::str_detect(anot_ho, "\\+3") hace lo mismo para los lanzamientos de 3 puntos que fueron efectivos. Para los puntos de Golden State, la función procede de forma analoga:
ldfs01 <- lapply(tb, function(x) fundf02(x)) # se aplica fundf02 a todos los elementos de la lista tb

df01 <- data.frame(do.call(rbind, ldfs01)) " unión vertical y conversión a data frame

Ahora obtenemos las proporciones:
df01 %>% group_by(t) %>% 
  summarise(ints_3p = sum(int_3p),
            convs_3p = sum(conv_3p)) %>% 
  mutate(pct_conv_3p = round((convs_3p/ints_3p) * 100, 1)) %>% 
  knitr::kable() %>% kableExtra::kable_styling()

La tabla arriba muestra la columna t corresponde a las iniciales del equipo; 'ints_3' es el número de intentos de lanzamientos de 3 puntos; 'convs_3p' es la cantidad de lanzamientos de 3 puntos convertidos y 'pct_conv_3p' es el porcentaje de lanzamientos efectivos. Para saber si esta diferencia en porcentaje de efectividad es significativa, podemos realizar un contraste, utilizando las facilidades del paquete infer. Para ello conviene crear un data frame en el que las cantidades de intentos fallidos o convertidos para cada equipo, estén en una sola columna, de modo que se puedan obtener las proporciones directamente. Escribimos una nueva función:
fundf03 <- function(x) {
  
  ho <- x %>% filter(str_detect(Inc_houst, "3-pt")) %>% 
    mutate( t3p = ifelse(str_detect(anot_ho, "\\+3"), "+3", "3-pt"),
            equipo = "ho") %>% 
    select(t3p, equipo, nro)
  
  gs <- x %>% filter(str_detect(Inc_gs, "3-pt")) %>% 
    mutate(t3p = ifelse(str_detect(anot_gs, "\\+3"), "+3", "3-pt"),
           equipo = "gs") %>% 
    select(t3p, equipo, nro)
  
  return(data.frame(rbind(ho, gs)))
  
}

ldfs02 <- lapply(tb, function(x) fundf03(x))

df02 <- data.frame(do.call(rbind, ldfs02)) # unión vertical

Con el siguiente código, producimos la diferencia en proporciones:
dif_p <- df02 %>% group_by(equipo) %>% 
  summarise(prop = mean(t3p == "+3")) %>% 
  summarise(diff(prop)) %>% 
  pull()
# [1] 0.0200617

A partir del siguiente código, para simulación, se hacen 5000 replicas de los resultados de lanzamientos que originan las proporciones, dichas replicas corresponden a permutaciones o reordenamientos generados aleatoriamente, asumiendo 'independencia' entre las proporciones obtenidas por los dos equipos, lo que sería la hipótesis 'nula'.
dist_nul <- df02 %>% 
  specify(t3p ~ equipo, success = "+3") %>% 
  hypothesize(null = "independence") %>% 
  generate(reps = 5000, type = "permute") %>% 
  calculate(stat = "diff in props", order = c("gs", "ho"))

Podemos graficar esta distribución simulada observando la ubicación de nuestra diferencia de proporciones, en dicha distribución:
dist_nul %>% 
  ggplot(aes(x = stat)) +
  geom_density() +
  geom_vline(xintercept = dif_p, color = "red") +
  theme_bw()

Obtener un p.valor
dist_nul %>% get_pvalue(obs_stat = dif_p, direction = "both")
# 0.633

Un intervalo de confianza:
dist_nul %>% get_ci()
# # A tibble: 1 x 2
#   `2.5%` `97.5%`
#       
#1 -0.0912  0.0866

Tanto el p_valor, como el intervalo de confianza, indican que no es posible rechazar la hipótesis, según la cual, las diferencias entre estas dos proporciones son deleznables. Observar la inmensa área cubierta por el p_valor:
dist_nul %>% visualize() + 
  shade_p_value(obs_stat = dif_p, direction = "both") +
  xlab("Diferencia en Proporciones") +
  ylab(NULL) +
  ggtitle("Lanzamientos 3pts, Houston\nGolden State") +
  theme_bw() +
  theme(
    plot.title = element_text(hjust = .5, vjust = .5),
    panel.border = element_blank()
  )

Bueno, son varias formas de observar que: a la luz de estos datos, al menos, la proporción de efectividad, en lanzamientos de 3 puntos, entre Warriors y Rockets no presenta diferencias significativas.

Comentarios

Entradas populares de este blog

R: Valores Faltantes en un Data Frame (Missing Values)

Son muy pocas las ocasiones en que las variables de un conjunto de datos están libres de observaciones faltantes ( NAs o missing values ). Es usual que al abordar una data nos interese saber la cantidad de ausencias, y también su caracterización, es decir, si esa ( no respuesta ) obedece a un patrón específico o es atribuible a causas aleatorías. El conteo de valores faltantes por variable, en un data frame, puede realizarse con pocas líneas de código como en el siguiente ejemplo, hecho con una data ficticia y funciones de la familia apply : # datos ficticios set.seed(4363) datos <- replicate(100, sample(c(rchisq(5, runif(1, 1, 100)), NA), 10, replace = TRUE), simplify = FALSE) datos <- do.call(rbind, datos) Luego el total de no respuesta por variable sería: datos <- data.frame(datos) unlist(lapply(datos, function(x) sum(is.na(x)))) # V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 # 18 18 18 15 15 19 14 14 15 14 El paquete magr

R: Simulacion de Variables Correlacionadas

En muchas situaciones suele ser conveniente generar un conjunto de variables con una correlación deseada. Algunos paquetes ofrecen medios para este fin de producir fake data ; pero también es perfectamente posible obtenerlas a través de métodos como la factorización (descomposicion) de Cholesky o la Descomposicion del Valor Singular (SVD: Singular Value Decomposition ). En el paquete de base de R existen funciones para hacer estos cálculos. La factorización de Cholesky, es un método con el que una matriz definida positiva y simetrica, es descompuesta en el producto de dos matrices triangulares (triangular inferior o superior) A = LL' (L es una matriz triangular inferior) A = U'U (U es una matriz triangular superior) siendo U' la traspuesta de U Mientras que la SVD (descomposición de valor singular) es una factorización de la forma: A = UΣV , la cuál generaliza la descomposición de autovalores. La implementación consiste simplemente en obtener el producto entre un vector

Optimizadores y Máximo Verosimil en R.

El proceso mediante el cual se obtienen estimaciones a partir de un conjunto de datos, frecuentemente involucra también un proceso de optimización. En lo más básico, por ejemplo, estimadores como la media o la mediana minimizan la suma de desviaciones al cuadrado y la suma de las desviaciones absolutas respectivamente Generalmente, se admite como un esquema rutinario del trabajo estadístico al momento de indagar sobre algún aspecto atinente a una población, asumir un modelo probabilístico, cuyos parámetros, siendo desconocidos, deben estimarse mediante la obtención de datos y posterior cálculo de los valores que mejor representen la data previamente recolectada. En ese último punto se halla frecuentemente implicada la optimización. La estimación por Máximo Verosimil, es generalmente obtenida mediante la aplicación de optimizadores no lineales, que son algoritmos que, por lo general, minimizan la función que se les pasa como argumento, debido a esto, para maximizar la función de verosi