Exámen práctico PRIA

Author

Indica tu nombre aquí

Published

December 5, 2023

## Librerías
library(tidyverse)
library(broom)
library(skimr)

1. Football-data: Datawrangling básico

En esta sección se realizarán tareas de descarga y lectura de datos, manipulación básica, selección de variables, conversiones de tipos, y exploración de la estructura de datos.

1.1 Descargando datos externos

Descarga los resultados de los partidos de fútbol de 1º y 2º división españolas y guárdalos en las variables SP1 y SP2 respectivamente.

SP1 <- read.csv("https://raw.githubusercontent.com/jesusturpin/curintel2324/main/data/SP1.csv")
SP2 <- read.csv("https://raw.githubusercontent.com/jesusturpin/curintel2324/main/data/SP2.csv")

1.2 Selección de datos (0.1 Pts)

Para ambos dataframes, selecciona las columnas Div, Date, HomeTeam, AwayTeam, FTHG, FTAG, PSCH, PSCA, que se corresponden con:

  • Div: Liga

  • Date: Fecha

  • HomeTeam: Local

  • AwayTeam: Visitante

  • FTHG: Marcador Local

  • FTAG: Marcador Visitante

  • FTR: Resultado: “H” (Victoria Local), “D” (Empate), “A”(Victoria Visitante)

  • PSCH: Cotización/cuota de la victoria equipo local justo al comienzo del encuentro (casa PS).

  • PSCD: Cotización/cuota del empate justo al comienzo del encuentro (casa PS).

  • PSCA: Cotización/cuota de la victoria visitante justo al comienzo del encuentro (casa PS).

Aclaración: La cotización o cuota se corresponde con el multiplicador o cantidad bruta que se pagaría sobre una apuesta acertada. Por ejemplo, si la victoria local tiene valor PSCH = 2.4, un apostante que haya pronosticado y acertado ese resultado, recibiría 2.4 € por cada euro apostado. Si apostó 10 €, recibiría 24 €, si su inversión fue 10 €, su ganancia neta serían 14.

SP1_1.2 <- SP1 %>%
  select(Div, Date, HomeTeam, AwayTeam, FTR, FTHG, FTAG, PSCH, PSCD, PSCA)

SP2_1.2 <- SP2 %>%
  select(Div, Date, HomeTeam, AwayTeam, FTR, FTHG, FTAG, PSCH, PSCD, PSCA)

1.3 Unión por filas (0.25 Pts)

Une por filas ambos dataframes y guárdalos en la variable SP_1.3

SP_1.3 <- SP1_1.2 %>%
  union(SP2_1.2)

1.4 Estructura resultante (0.1 Pts)

Muestra la estructura del dataframe SP_1.3 e indica:

  • Número de filas
  • Número de columnas
SP_1.3 %>%
  glimpse()
Rows: 326
Columns: 10
$ Div      <chr> "SP1", "SP1", "SP1", "SP1", "SP1", "SP1", "SP1", "SP1", "SP1"…
$ Date     <chr> "11/08/2023", "11/08/2023", "12/08/2023", "12/08/2023", "12/0…
$ HomeTeam <chr> "Almeria", "Sevilla", "Sociedad", "Las Palmas", "Ath Bilbao",…
$ AwayTeam <chr> "Vallecano", "Valencia", "Girona", "Mallorca", "Real Madrid",…
$ FTR      <chr> "A", "A", "D", "D", "A", "A", "A", "D", "H", "H", "A", "H", "…
$ FTHG     <int> 0, 1, 1, 1, 0, 0, 1, 0, 1, 3, 0, 1, 1, 1, 0, 3, 2, 0, 4, 0, 0…
$ FTAG     <int> 2, 2, 1, 1, 2, 2, 2, 0, 0, 1, 1, 0, 1, 3, 2, 0, 0, 0, 3, 2, 0…
$ PSCH     <dbl> 2.66, 1.94, 1.85, 2.64, 3.44, 2.16, 1.75, 7.46, 2.33, 1.31, 2…
$ PSCD     <dbl> 3.25, 3.46, 3.60, 2.93, 3.50, 3.22, 4.10, 4.04, 2.96, 5.90, 3…
$ PSCA     <dbl> 2.96, 4.58, 4.91, 3.32, 2.24, 4.02, 4.79, 1.55, 3.91, 10.50, …

1.5 Paquetes y librerías (0.25 Pts)

Instala el paquete skimr, carga la librería y utiliza la función skim sobre el dataframe SP_1.3. ¿Hay alguna variable con huecos o valores perdidos (NULL, NA, espacios, etc.)?

skim(SP_1.3)
Data summary
Name SP_1.3
Number of rows 326
Number of columns 10
_______________________
Column type frequency:
character 5
numeric 5
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
Div 0 1 3 3 0 2 0
Date 0 1 10 10 0 70 0
HomeTeam 0 1 5 12 0 42 0
AwayTeam 0 1 5 12 0 42 0
FTR 0 1 1 1 0 3 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
FTHG 0 1 1.48 1.20 0.00 1.00 1.00 2.00 6.0 ▇▃▂▁▁
FTAG 0 1 1.10 1.07 0.00 0.00 1.00 2.00 7.0 ▇▂▁▁▁
PSCH 0 1 2.45 0.97 1.18 1.86 2.20 2.76 7.5 ▇▃▁▁▁
PSCD 0 1 3.50 0.73 2.71 3.09 3.30 3.64 8.0 ▇▁▁▁▁
PSCA 0 1 4.29 2.08 1.44 2.96 3.95 4.96 16.5 ▇▃▁▁▁

1.6 Tipos de datos y conversiones (0.5 Pts)

De las variables anteriores, indica cuáles son de los siguientes tipos:

  • Numéricas discretas:

  • Numéricas contínuas:

  • Categóricas nominales:

  • Categóricas ordinales:

Convierte el tipo de la variable Date a tipo fecha (Date) . Convierte a factor el resto de variables categóricas. Una de ellas es ordinal, por lo que no debes olvidar asignar los niveles.

Guarda el dataframe resultante en la variable SP_1.6

SP_1.6 <- SP_1.3 %>%
  mutate(Date = as.Date(Date, format = "%d/%m/%Y"),
         Div = factor(Div, levels = c("SP1", "SP2")))

Ahora aplica la función summary, e indica:

  • Nº partidos de 1ª:
  • Nº partidos de 2ª:
  • Fecha 1º partido:
  • Fecha último partido:
  • Máximo nº de goles:
  • Cuota más alta: ¿a qué resultado: H, D ó A?
summary(SP_1.6)
  Div           Date              HomeTeam           AwayTeam        
 SP1:139   Min.   :2023-08-11   Length:326         Length:326        
 SP2:187   1st Qu.:2023-09-03   Class :character   Class :character  
           Median :2023-10-01   Mode  :character   Mode  :character  
           Mean   :2023-10-02                                        
           3rd Qu.:2023-10-29                                        
           Max.   :2023-11-29                                        
     FTR                 FTHG            FTAG            PSCH      
 Length:326         Min.   :0.000   Min.   :0.000   Min.   :1.180  
 Class :character   1st Qu.:1.000   1st Qu.:0.000   1st Qu.:1.860  
 Mode  :character   Median :1.000   Median :1.000   Median :2.195  
                    Mean   :1.479   Mean   :1.095   Mean   :2.448  
                    3rd Qu.:2.000   3rd Qu.:2.000   3rd Qu.:2.760  
                    Max.   :6.000   Max.   :7.000   Max.   :7.500  
      PSCD            PSCA       
 Min.   :2.710   Min.   : 1.440  
 1st Qu.:3.090   1st Qu.: 2.962  
 Median :3.305   Median : 3.945  
 Mean   :3.504   Mean   : 4.293  
 3rd Qu.:3.638   3rd Qu.: 4.957  
 Max.   :8.000   Max.   :16.500  

2. Football-data: Datawrangling avanzado

En esta sección, se crearán variables y métricas nuevas a partir de los datos originales.

2.1 Retorno y comisión (0.25 Pts)

El retorno indica la proporción de premios que la sala retorna sobre lo recaudado al conjunto de sus apostantes en un punto de equilibrio para un partido y unas cotizaciones al local, empate y visitante registradas en las columnas PSCH, PSCD y PSCA respectivamente. El valor lógicamente será un número entre 0 y 1, pues si es superior a 1, ¡la banca pierde! y como te imaginarás, eso no ocurre.

La fórmula del retorno para cotizaciones en formato decimal, como vienen en football-data es:

\[Ret = \frac{1}{{\frac{1}{PSCH} + \frac{1}{PSCD} + \frac{1}{PSCA}}}\]

Si el retorno es la proporción que reparte de lo recaudado por evento, la comisión promedio que gana la sala es:

\[Fee = 1 - Ret\] Crea las columnas Ret y Fee según las fórmulas descritas. Guarda el resultado en SP_2.1

SP_2.1 <- SP_1.6 %>%
  mutate(Ret = 1/(1/PSCH + 1/PSCD + 1/PSCA)) %>%
  mutate(Fee = 1 - Ret)

2.2 Cuotas ajustadas (0.5 Pts)

Ahora crea 3 nuevas columnas PSCH_adj, PSCD_adj y PSCA_adj cuyo que son las cuotas ajustadas, es decir, las cuotas originales ofrecidas por una determinada sala de apuestas si eliminamos comisiones implícitas. Si en esas cuotas calculásemos Fee y Ret, los resultados serían 0 y 1 respectivamente. Puedes verificar si lo has hecho bien si calculas Ret y obtienes siempre 1. Guarda el dataframe resultante en SP_2.2.

Sus fórmulas son:

  • \(PSCH_{\text{adj}} = PSCH \left( \frac{1}{PSCH} + \frac{1}{PSCD} + \frac{1}{PSCA} \right)\)
  • \(PSCD_{\text{adj}} = PSCD \left( \frac{1}{PSCH} + \frac{1}{PSCD} + \frac{1}{PSCA} \right)\)
  • \(PSCA_{\text{adj}} = PSCA \left( \frac{1}{PSCH} + \frac{1}{PSCD} + \frac{1}{PSCA} \right)\)

Para no repetir cálculos, vamos a definir la variable probabilidades implícitas totales Total_prob_imp:

\(Total\_prob\_imp = \frac{1}{PSCH} + \frac{1}{PSCD} + \frac{1}{PSCA}\)

De este modo, las fórmulas quedarían:

  • \(PSCH_{\text{adj}} = Total\_prob\_imp \cdot PSCH\)
  • \(PSCD_{\text{adj}} = Total\_prob\_imp \cdot PSCD\)
  • \(PSCA_{\text{adj}} = Total\_prob\_imp \cdot PSCA\)

Elimina la variable Total_prob_imp si las has creado.

SP_2.2 <- SP_2.1 %>%
  mutate(Total_prob_imp = 1/PSCH + 1/PSCD + 1/PSCA) %>%
  mutate(PSCH_adj = PSCH*Total_prob_imp,
         PSCD_adj = PSCD*Total_prob_imp,
         PSCA_adj = PSCA*Total_prob_imp) %>%
  select(-Total_prob_imp)

2.3 Probabilidades estimadas por la casa de apuestas (0.25 Pts)

Una vez calculadas las cuotas ajustadas sin margen, calcular las probabilidades es tan simple como calcular la inversa de la cuota de cada posible evento:

  • \(P_H = \frac{1}{PSCH_{\text{adj}}}\)
  • \(P_D = \frac{1}{PSCD_{\text{adj}}}\)
  • \(P_A = \frac{1}{PSCA_{\text{adj}}}\)

Añade 3 nuevas columnas llamadas PH, PD y PA que se corresponden con las probabilidades de victoria local, empate o victoria visitante según se estima a partir de las cuotas.

Ordena los resultados por Fecha, División y por orden alfabético del equipo local. Muestra las 10 primeras filas

SP_2.3 <- 
  SP_2.2 %>%
  mutate(PH = 1/PSCH_adj,
         PD = 1/PSCD_adj,
         PA = 1/PSCA_adj) %>%
  arrange(Date, Div, HomeTeam) 

2.4 Pivote o join (1 Pts)

Ahora el desafío consiste en dividir el dataframe de partidos a equipos. Cada fila ahora se debe duplicar para mostrar la información desde la perspectiva del equipo local en una y desde el equipo visitante en la otra fila.

Se eliminan las columnas HomeTeam y AwayTeam, sustituyéndose por una nueva llamada HomeAway y otra con los valores llamada Equipo. Coloca las nuevas columnas en la 3º y 4º posición.

SP_2.4 <- SP_2.3 %>%
  pivot_longer(cols = c("HomeTeam", "AwayTeam"), names_to = "HomeAway", values_to = "Equipo") %>%
  select(Div, Date, HomeAway, Equipo, dplyr::everything())

2.5 Métricas: puntos (Pts), puntos esperados (EPts) (1 Pts)

Crea las siguientes variables:

  • Puntos (Pts) 3 por cada victoria local y 1 por cada empate
  • Puntos esperados (EPts). En este caso, no importa el resultado. Los puntos esperados se calculan multiplicando 3 por la probabilidad de victoria y sumando la probabilidad de empate. Si un equipo tiene una probabilidad de victoria del 50 % y su probabilidad de empate es 25 %, sus puntos esperados serían: 0.5x3 + 0.25 = 1.75
SP_2.5 <- SP_2.4 %>%
  mutate(EPts = (if_else(HomeAway == "HomeTeam", PH*3, PA*3)) + PD) %>%
  mutate(Pts = 3*(HomeAway == "HomeTeam")*(FTR == "H") + 1*(FTR == "D") +
           3*(HomeAway == "AwayTeam")*(FTR == "A")) %>%
  select(Div, Date, Equipo, Pts, EPts, everything()) 

3. Football-data: Resumen y Visualización

3.1 Tabla de clasificación por EPts (0.5 Pts)

Implementa una función llamada ranking_epts que reciba 4 parámetros e imprima el ranking según los parámetros:

  • “team.results”: data frame con los resultados partido a partido de cada equipo
  • “liga”: cadena de texto “SP1” o “SP2”
  • “orden.epts”. Opcional, su valor por defecto es FALSE.
  • “invertir.orden”. Opcional, su valor por defecto es FALSE.

La salida tiene que ser un dataframe con 4 columnas en este orden:

  • Puesto: ranking o clasificación 1,2,3…
  • Equipo
  • Pts
  • EPts

La clasificación se ordena por Pts o por EPts, según el parámetro opcional “orden.epts” y el orden es de mayor a menor puntuación, excepto si el parámetro invertir.orden está a TRUE.

ranking_epts <- function(team.results, liga, 
                         orden.epts = FALSE, invertir.orden = FALSE) {
  output <- team.results %>%
  filter(Div == liga) %>%
  group_by(Equipo) %>%
  summarise(Pts = sum(Pts), EPts = sum(EPts)) 
  if(orden.epts) {
    output <- output %>%
      arrange(desc(EPts))
    if(invertir.orden) {
      output <- output %>% arrange(EPts)
    }
  } else {
    output <- output %>%
      arrange(desc(Pts))
    if(invertir.orden) {
      output <- output %>% arrange(Pts)
    }
  }
  return(output %>%
           mutate(Puesto = row_number()) %>%
         select(Puesto, everything()))
}
ranking_epts(SP_2.5, "SP1", orden.epts = FALSE, invertir.orden = FALSE)
# A tibble: 20 × 4
   Puesto Equipo        Pts  EPts
    <int> <chr>       <dbl> <dbl>
 1      1 Girona         35  21.2
 2      2 Real Madrid    35  28.4
 3      3 Ath Madrid     31  24.9
 4      4 Barcelona      31  29.1
 5      5 Ath Bilbao     25  22.8
 6      6 Sociedad       25  23.0
 7      7 Betis          24  19.1
 8      8 Getafe         19  16.0
 9      9 Valencia       19  19.3
10     10 Vallecano      19  16.6
11     11 Las Palmas     18  13.8
12     12 Alaves         15  16.0
13     13 Villarreal     15  20.6
14     14 Osasuna        14  16.2
15     15 Sevilla        12  18.7
16     16 Cadiz          11  13.6
17     17 Mallorca       10  15.6
18     18 Celta           8  17.5
19     19 Granada         7  14.0
20     20 Almeria         3  14.2

3.2 Tabla de clasificación por Ratio Pts/EPts (0.5 Pts)

Crea la variable R_Pts_EP e imprime la clasificación por ese criterio. Utiliza ambas ligas en conjunto

  • Ratio Pts vs Epts (R_Pts_EP). Dividir los puntos conseguidos entre los esperados. Un valor por encima de 1 indica que el equipo lo ha hecho mejor de lo esperado y por debajo de 1 indica lo contrario.
SP_2.5 %>%
  group_by(Equipo) %>%
  summarise(R_Pts_EP = (sum(Pts))/(sum(EPts))) %>%
  arrange(desc(R_Pts_EP)) %>%
  mutate(Puesto = row_number()) %>%
  select(Puesto, everything())
# A tibble: 42 × 3
   Puesto Equipo      R_Pts_EP
    <int> <chr>          <dbl>
 1      1 Girona          1.65
 2      2 Leganes         1.54
 3      3 Sp Gijon        1.31
 4      4 Las Palmas      1.31
 5      5 Valladolid      1.26
 6      6 Betis           1.26
 7      7 Ferrol          1.24
 8      8 Ath Madrid      1.24
 9      9 Real Madrid     1.23
10     10 Santander       1.20
# ℹ 32 more rows

3.3 Visualización (0.8 Pts)

Realiza un diagrama de dispersión enfrentando el promedio de puntos Pts (y) vs Promedio de puntos esperados EPts (x). Usa un lienzo para cada gráfica, según su liga.

  • Dibuja un diagrama de dispersión enfrentando Pts vs EPts en cada equipo.
  • El color se mapea según si el ratio Pts/EPts es mayor que uno.
  • Mapea el tamaño según el valor absoluto de Pts - Epts
  • Añade una línea recta con pendiente 1 y que pase por el origen.
  • Crea facetas para representar cada liga en un lienzo diferente
  • Usa text_repel para mostrar los nombres de los equipos
library(ggrepel)
SP_2.5 %>%
  group_by(Div, Equipo) %>%
  summarise(Pts =  mean(Pts),
            EPts = mean(EPts),
            R_Pts_EP = mean(Pts/EPts),
            diff_pts = mean(abs(Pts - EPts)),
            .groups = "drop") %>%
  ggplot(aes(EPts, Pts, size = diff_pts, color = R_Pts_EP > 1)) +
  geom_point(alpha = 0.75)+
  geom_abline(intercept = 0, slope = 1, color = "darkgreen", linewidth = 1)+
  geom_text_repel(aes(x = EPts, y = Pts, label = Equipo), size = 3, vjust = -1) +
  facet_wrap(~ Div) +
  theme_bw()