## Librerías
library(tidyverse)
library(broom)
library(skimr)
Exámen práctico PRIA
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.
<- read.csv("https://raw.githubusercontent.com/jesusturpin/curintel2324/main/data/SP1.csv")
SP1 <- read.csv("https://raw.githubusercontent.com/jesusturpin/curintel2324/main/data/SP2.csv") SP2
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.
.2 <- SP1 %>%
SP1_1select(Div, Date, HomeTeam, AwayTeam, FTR, FTHG, FTAG, PSCH, PSCD, PSCA)
.2 <- SP2 %>%
SP2_1select(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
.3 <- SP1_1.2 %>%
SP_1union(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
.3 %>%
SP_1glimpse()
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)
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
.6 <- SP_1.3 %>%
SP_1mutate(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
.1 <- SP_1.6 %>%
SP_2mutate(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.
.2 <- SP_2.1 %>%
SP_2mutate(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
.3 <-
SP_2.2 %>%
SP_2mutate(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.
.4 <- SP_2.3 %>%
SP_2pivot_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
.5 <- SP_2.4 %>%
SP_2mutate(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.
<- function(team.results, liga,
ranking_epts orden.epts = FALSE, invertir.orden = FALSE) {
<- team.results %>%
output 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 %>% arrange(EPts)
output
}else {
} <- output %>%
output arrange(desc(Pts))
if(invertir.orden) {
<- output %>% arrange(Pts)
output
}
}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.
.5 %>%
SP_2group_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)
.5 %>%
SP_2group_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()