Direkt zum Hauptbereich

Bayern wird gegen Gladbach gewinnen? diese Prognose muss sich noch bestätigen


Den nächsten Bundesliga-Spieltag vorhersagen? Bestimmt nicht einfach, mit den Spielerwechseln, Transfers oder auch möglichen Verletzungen. Nun ich versuche es auf relativ simple Weise. Leider mit nicht großem Erfolg; dennoch gibts am Ende des Artikels eine Prognose zum nächsten Spieltag der ersten Bundesliga. Die nächsten Tage werden zeigen, wie gut die Prognose war. 

Für die Daten benutze ich die Seite fbref.com; der Direktlink funktioniert über die Funktion Embed this Table, wobei die Umlaute hier falsch formattiert sind. Dies korrigier ich noch mit der gsub()-Funktion. Mich interessiert zwar nur die erste Bundesliga. Ich ziehe aber auch die Daten für die 2. Bundesliga an, da auch die Teams mit Auf-und Abstiegen berücksichtigt werden müssen. Zur Einfachheit unterscheide ich im späteren Modell aber nicht nach Liga. Die Daten sehen schließlich wiefolgt aus:

##         Date         Home            Away Home_Score Away_Score Result year
## 1 2021-08-13   M'Gladbach   Bayern Munich          1          1      D 2021
## 2 2021-08-14     Augsburg      Hoffenheim          0          4      A 2021
## 3 2021-08-14 Union Berlin      Leverkusen          1          1      D 2021
## 4 2021-08-14    Wolfsburg          Bochum          1          0      H 2021
## 5 2021-08-14      Arminia        Freiburg          0          0      D 2021
## 6 2021-08-14    Stuttgart Greuther Fuerth          5          1      H 2021

Als erstes schauen wir auf die Durchschnittswerte, wie auch hier beschrieben. Wir sehen schon den bekannten Heimvorteil im Datensatz, der aber verschieden stark über die Jahre hinweg ist. Dies ist v.a. deswegen wichtig, weil wir mit den Testdaten vor dem Juni 2021 die Trainingsdaten von nach Juni 2021 vorhersagen wollen. Wir sehen direkt, dass der Heimvorteil im letzten Jahr ohne Zuschauer abgenommen hat.

## # A tibble: 4 × 3
##   year  avg_home_goals avg_away_goals
##   <chr>          <dbl>          <dbl>
## 1 2019            1.68           1.41
## 2 2020            1.56           1.40
## 3 2021            1.74           1.31
## 4 2022           NA             NA

Der Heimvorteil ist auch in der folgenden Visualisierung als Dichtefunktion gut sichtbar.

  

Perfekt. Es zeigt sich also, dass die Daten einige Informationen hergeben (können). Zeit für eine tiefere Analyse. Als einfaches Modell nehm ich jetzt für jedes Team die Durchschnittstore geschossen als Angriffs- (alpha) und kassiert als Verteidigungsstärke (beta). Dafür nehme ich Testdaten von vor dem Juni 2021, um später mit den Ergebnissen von nach dem Juni das Modell zu evaluieren.

## # A tibble: 6 × 3
##   team          alpha  beta
##   <chr>         <dbl> <dbl>
## 1 Arminia       1.91  0.882
## 2 Augsburg      1.32  1.85 
## 3 Bayern Munich 2.94  0.941
## 4 Bochum        1.75  1.32 
## 5 Braunschweig  0.882 1.74 
## 6 Darmstadt 98  1.63  1.44

Im Folgenden nun die vorhergesagten Ergebnisse mit folgender Methodik:

gamma <- sum(Home_Score) / sum(Away_Score) #Heimvorteil
Home_Score_e = round(team_alphas[Home]* team_betas[Away]* gamma,3))
Away_Score_e = round(team_alphas[Away]* team_betas[Home],3))
##         Date         Home            Away Home_Score Away_Score Result
## 1 2021-08-13   M'Gladbach   Bayern Munich          1          1      D
## 2 2021-08-14     Augsburg      Hoffenheim          0          4      A
## 3 2021-08-14 Union Berlin      Leverkusen          1          1      D
## 4 2021-08-14    Wolfsburg          Bochum          1          0      H
## 5 2021-08-14      Arminia        Freiburg          0          0      D
## 6 2021-08-14    Stuttgart Greuther Fuerth          5          1      H
##   Home_Score_e Away_Score_e Result_e
## 1        2.140        3.460        A
## 2        2.417        2.888        A
## 3        1.828        3.061        A
## 4        2.189        2.368        A
## 5        3.096        1.246        H
## 6        2.796        2.039        H

Die Güte messe ich ganz einfach mit dem Anteil der richtig vorhergesagten Resultate Heimsieg, Unentschieden oder Gastsieg.

##   correct total accuracy
## 1      63   153   41.18%

Oder im Folgenden als Confusion matrix.

##       predicted
## actual  A  H
##      A 16 25
##      D 16 20
##      H 29 47

Anwendung der Poisson-Verteilung

Die Verteilung der Tore lässt sich gut mit einer Poisson-Verteilung annähren. Die Poisson-Wahrscheinlichkeitsverteilung erlaubt es die Anzahl von Toren zu modellieren, die unabhängig voneinander in den 90 Minuten Spielzeit eintreten. Die Tore werden im folgenden mit den Durchschnittswerten vergangener Spiele (des gesamten Datensatzes) angenähert, wie hier beschrieben:

 

Dasselbe können wir pro Team machen; im Folgenden zB für Freiburg. Es wird also schon ersichtlich, wie wir ein Modell bilden können. 

Die Differenz zw. zwei Poisson-Verteilungen ist eine Skellam-Verteilung. Dies erlaubt uns die Wahrscheinlichkeit spezifischer Events zu berechnen. Zum Beispiel liegt die Wahrscheinlichkeit, dass das Heim-Team mit einem Tor mehr gewinnt bei 21%.

dskellam(1,mean(dataset_poisson$Home_Score),mean(dataset_poisson$Away_Score))
## [1] 0.2128509

Und visualisiert als Grafik:

Im nächsten Schritt baue ich ein allgemeineres Poisson-Regressions-Modell mit dem Trainingsdatensatz und ohne die Unabhängigkeit zwischen den Spielen. Des Ergebnis sieht wiefolgt aus:

## 
## Call:
## glm(formula = goals ~ home + team + opponent, family = poisson(link = log), 
##     data = .)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.4943  -0.9307  -0.1576   0.5838   3.7918  
## 
## Coefficients:
##                          Estimate Std. Error z value Pr(>|z|)    
## (Intercept)              0.054707   0.223481   0.245 0.806617    
## home                     0.158243   0.037922   4.173 3.01e-05 ***
## teamAugsburg            -0.018361   0.215829  -0.085 0.932205    
## teamBayern Munich        0.749197   0.185366   4.042 5.31e-05 ***
## teamBochum              -0.087549   0.154780  -0.566 0.571643    
## teamBraunschweig        -0.774171   0.221668  -3.492 0.000479 ***
## teamDarmstadt 98        -0.152617   0.156733  -0.974 0.330188    
## teamDortmund             0.584118   0.190434   3.067 0.002160 ** 
## teamDresden             -0.677719   0.216227  -3.134 0.001723 ** 
## teamDuesseldorf         -0.204933   0.168526  -1.216 0.223974    
## teamEint Frankfurt       0.250262   0.203246   1.231 0.218202    
## teamErzgebirge Aue      -0.361289   0.163302  -2.212 0.026939 *  
## teamFreiburg             0.029159   0.212588   0.137 0.890903    
## teamGreuther Fuerth     -0.122466   0.155720  -0.786 0.431602    
## teamHamburger SV         0.024132   0.151898   0.159 0.873773    
## teamHannover 96         -0.188262   0.157806  -1.193 0.232871    
## teamHeidenheim          -0.336645   0.160709  -2.095 0.036193 *  
## teamHertha BSC           0.042059   0.212587   0.198 0.843169    
## teamHoffenheim           0.134984   0.207914   0.649 0.516190    
## teamHolstein Kiel       -0.173189   0.156550  -1.106 0.268604    
## teamIngolstadt 04       -0.155618   0.403027  -0.386 0.699405    
## teamJahn R'burg         -0.392337   0.164477  -2.385 0.017062 *  
## teamKarlsruher          -0.297132   0.161162  -1.844 0.065228 .  
## teamKoeln                0.131551   0.202779   0.649 0.516507    
## teamLeverkusen           0.266291   0.201876   1.319 0.187141    
## teamM'Gladbach           0.340994   0.198776   1.715 0.086259 .  
## teamMainz 05            -0.038740   0.216995  -0.179 0.858309    
## teamNuernberg           -0.347658   0.162457  -2.140 0.032355 *  
## teamOsnabrueck          -0.461586   0.166303  -2.776 0.005511 ** 
## teamPaderborn 07        -0.212756   0.168871  -1.260 0.207717    
## teamRB Leipzig           0.543205   0.191588   2.835 0.004579 ** 
## teamSandhausen          -0.428131   0.165718  -2.583 0.009781 ** 
## teamSchalke 04          -0.193306   0.225112  -0.859 0.390500    
## teamSt. Pauli           -0.336270   0.162567  -2.068 0.038593 *  
## teamStuttgart           -0.034356   0.177801  -0.193 0.846779    
## teamUnion Berlin        -0.117122   0.220795  -0.530 0.595796    
## teamW'burg Kickers      -0.551908   0.206929  -2.667 0.007650 ** 
## teamWehen               -0.327352   0.194266  -1.685 0.091975 .  
## teamWerder Bremen       -0.121774   0.210675  -0.578 0.563254    
## teamWolfsburg            0.028092   0.212588   0.132 0.894870    
## opponentAugsburg         0.307533   0.241189   1.275 0.202284    
## opponentBayern Munich   -0.312106   0.271209  -1.151 0.249817    
## opponentBochum           0.419601   0.211216   1.987 0.046968 *  
## opponentBraunschweig     0.676029   0.225074   3.004 0.002668 ** 
## opponentDarmstadt 98     0.500293   0.209052   2.393 0.016704 *  
## opponentDortmund        -0.081014   0.258250  -0.314 0.753745    
## opponentDresden          0.622305   0.225181   2.764 0.005717 ** 
## opponentDuesseldorf      0.397933   0.214520   1.855 0.063598 .  
## opponentEint Frankfurt   0.273837   0.242832   1.128 0.259455    
## opponentErzgebirge Aue   0.518043   0.208306   2.487 0.012885 *  
## opponentFreiburg         0.016915   0.252143   0.067 0.946516    
## opponentGreuther Fuerth  0.405993   0.211508   1.920 0.054919 .  
## opponentHamburger SV     0.428030   0.211229   2.026 0.042726 *  
## opponentHannover 96      0.518178   0.208559   2.485 0.012971 *  
## opponentHeidenheim       0.327480   0.211804   1.546 0.122070    
## opponentHertha BSC       0.244968   0.243410   1.006 0.314223    
## opponentHoffenheim       0.142779   0.247321   0.577 0.563735    
## opponentHolstein Kiel    0.435300   0.209698   2.076 0.037908 *  
## opponentIngolstadt 04    0.671511   0.453140   1.482 0.138366    
## opponentJahn R'burg      0.564738   0.207180   2.726 0.006414 ** 
## opponentKarlsruher       0.511618   0.208549   2.453 0.014158 *  
## opponentKoeln            0.393858   0.236169   1.668 0.095376 .  
## opponentLeverkusen      -0.035274   0.255006  -0.138 0.889982    
## opponentM'Gladbach      -0.125478   0.259425  -0.484 0.628616    
## opponentMainz 05         0.337805   0.240174   1.407 0.159575    
## opponentNuernberg        0.590020   0.206315   2.860 0.004239 ** 
## opponentOsnabrueck       0.566398   0.206695   2.740 0.006139 ** 
## opponentPaderborn 07     0.449285   0.213477   2.105 0.035325 *  
## opponentRB Leipzig      -0.187370   0.263306  -0.712 0.476708    
## opponentSandhausen       0.553453   0.207394   2.669 0.007617 ** 
## opponentSchalke 04       0.217069   0.244008   0.890 0.373681    
## opponentSt. Pauli        0.567708   0.207185   2.740 0.006142 ** 
## opponentStuttgart        0.309796   0.240627   1.287 0.197936    
## opponentUnion Berlin     0.220280   0.244008   0.903 0.366655    
## opponentW'burg Kickers   0.841408   0.219529   3.833 0.000127 ***
## opponentWehen            0.752071   0.221070   3.402 0.000669 ***
## opponentWerder Bremen    0.382074   0.234598   1.629 0.103391    
## opponentWolfsburg       -0.004646   0.253058  -0.018 0.985352    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for poisson family taken to be 1)
## 
##     Null deviance: 2319.0  on 1855  degrees of freedom
## Residual deviance: 2070.3  on 1778  degrees of freedom
## AIC: 5739.9
## 
## Number of Fisher Scoring iterations: 5

Mich interessiert vor allem die Spalte Estimate; sie ist ähnlich wie der Steigungskoeffizient einer linearen Regression. Ähnlich wie bei einer logistischen Regression zählt der Exponent: positiv bedeutet mehr Tore, Werte näher an Null bedeuten einen negativeren Effekt. Wir sehen, dass home einen Estimate-Wert von 0.19 hat; dass Heim-Teams also mehr Tore schießen als Gastteams: genauer gesagt exp(0.19)=1.2 mal mehr. Auch die einzelnen Teams fließen mit ihrer Stärke bzw Schwäche ein: Bayern München hat einen estimate von 0.9, Nürnberg von -0.2; München schießt also Tore wie der Durchschnitt, Nürnberg weniger. Ähnlich lässt sich das mit den opponentTeams und der Verteidigungsstärke tun. Nun können wir wieder Vorhersagen machen. Ich mache direkt eine Simulation für verschiedene Ergebnisse: anbei wieder Freiburg gegen Stuttgart. Die Matrix zeigt die Wahrscheinlichkeit an Toren für Freiburg (Reihe) und Stuttgart (Spalte). Die Diagonale ist ein Unentschieden, unter der Diagonale ein Freiburg-Sieg, darüber ein Stuttgart-Sieg

##   [1] "6.238%"  "10.832%" "9.406%"  "5.444%"  "2.364%"  "0.821%"  "0.238%" 
##   [8] "0.059%"  "0.013%"  "0.002%"  "0.000%"  "6.475%"  "11.244%" "9.763%" 
##  [15] "5.651%"  "2.453%"  "0.852%"  "0.247%"  "0.061%"  "0.013%"  "0.003%" 
##  [22] "0.000%"  "3.360%"  "5.835%"  "5.067%"  "2.933%"  "1.273%"  "0.442%" 
##  [29] "0.128%"  "0.032%"  "0.007%"  "0.001%"  "0.000%"  "1.163%"  "2.019%" 
##  [36] "1.753%"  "1.015%"  "0.441%"  "0.153%"  "0.044%"  "0.011%"  "0.002%" 
##  [43] "0.000%"  "0.000%"  "0.302%"  "0.524%"  "0.455%"  "0.263%"  "0.114%" 
##  [50] "0.040%"  "0.011%"  "0.003%"  "0.001%"  "0.000%"  "0.000%"  "0.063%" 
##  [57] "0.109%"  "0.094%"  "0.055%"  "0.024%"  "0.008%"  "0.002%"  "0.001%" 
##  [64] "0.000%"  "0.000%"  "0.000%"  "0.011%"  "0.019%"  "0.016%"  "0.009%" 
##  [71] "0.004%"  "0.001%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
##  [78] "0.002%"  "0.003%"  "0.002%"  "0.001%"  "0.001%"  "0.000%"  "0.000%" 
##  [85] "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
##  [92] "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
##  [99] "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
## [106] "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
## [113] "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%"  "0.000%" 
## [120] "0.000%"  "0.000%"

Damit können wir nun wiefolgt die Wahrscheinlichkeiten berechnen:

tibble(Freiburg_win = percent(sum(game_sim[lower.tri(game_sim)]), accuracy=0.1),
       Draw = percent(sum(diag(game_sim)), accuracy=0.1),
       Stuttgart_win = percent(sum(game_sim[upper.tri(game_sim)]), accuracy=0.1))
## # A tibble: 1 × 3
##   Freiburg_win   Draw       Stuttgart_win
##   <chr>          <chr>      <chr>        
## 1 53.7%          23.7%      22.6%

Genauso prognostiziere ich nun die Spiele des Testdatensatzes und bekomme folgendes Ergebnis:

## # A tibble: 6 × 11
## # Rowwise: 
##   Date       Home  Away  Home_Score Away_Score Result year  Sim_HomeWin Sim_Draw
##   <chr>      <chr> <chr>      <dbl>      <dbl> <chr>  <chr>       <dbl>    <dbl>
## 1 2021-08-13 M'Gl… Baye…          1          1 D      2021        0.245    0.218
## 2 2021-08-14 Augs… Hoff…          0          4 A      2021        0.328    0.239
## 3 2021-08-14 Unio… Leve…          1          1 D      2021        0.234    0.239
## 4 2021-08-14 Wolf… Boch…          1          0 H      2021        0.601    0.217
## 5 2021-08-14 Armi… Frei…          0          0 D      2021        0.402    0.279
## 6 2021-08-14 Stut… Greu…          5          1 H      2021        0.496    0.231
## # … with 2 more variables: Sim_AwayWin <dbl>, Result_Sim <chr>

Die Güte messe ich wieder ganz einfach mit dem Anteil der richtig vorhergesagten Resultate Heimsieg, Unentschieden oder Gastsieg.

##   correct  total   accuracy
## 1      74    153   48.4%

Schade, leider auch nicht besser, wie auch die Matrix zeigt:

##       predicted
## actual  A  H
##      A 16 25
##      D 16 20
##      H 29 47

Leider ist das Modell also nicht besser als ein Münzwurf. Dennoch versuche ich mich am nächsten Spieltag mit einem Modell, das am gesamten Datensatz gebildet wurde.

## # A tibble: 9 × 7
## # Rowwise: 
##   Date       Home            Away    Sim_HomeWin Sim_Draw Sim_AwayWin Sim_Result
##   <chr>      <chr>           <chr>   <chr>       <chr>    <chr>       <chr>     
## 1 2022-01-07 Bayern Munich   M'Glad… 76.70%      13.61%   9.66%       H         
## 2 2022-01-08 Greuther Fuerth Stuttg… 32.40%      23.70%   43.90%      A         
## 3 2022-01-08 Hoffenheim      Augsbu… 59.34%      21.02%   19.64%      H         
## 4 2022-01-08 RB Leipzig      Mainz … 69.10%      17.78%   13.12%      H         
## 5 2022-01-08 Freiburg        Arminia 50.23%      27.67%   22.10%      H         
## 6 2022-01-08 Leverkusen      Union … 61.51%      20.55%   17.94%      H         
## 7 2022-01-08 Eint Frankfurt  Dortmu… 26.38%      19.96%   53.66%      A         
## 8 2022-01-09 Hertha BSC      Koeln   40.90%      22.82%   36.27%      H         
## 9 2022-01-09 Bochum          Wolfsb… 29.70%      26.24%   44.06%      A


Anbei noch der komplette Code:

library(dplyr)
library(ggplot2)
library(tidyr)
library(lubridate)
library(xml2)
library(rvest)
library(reshape2)
library(skellam)

# datapull using the Link on fbref.com from Bundesliga + "Embed This Table"
url_BL1_2021_2022 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F20%2Fschedule%2FBundesliga-Scores-and-Fixtures&div=div_sched_11193_1"
url_BL2_2021_2022 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F33%2Fschedule%2F2-Bundesliga-Scores-and-Fixtures&div=div_sched_11194_1"
url_BL1_2020_2021 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F20%2F10737%2Fschedule%2F2020-2021-Bundesliga-Scores-and-Fixtures&div=div_sched_all"
url_BL2_2020_2021 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F33%2F10745%2Fschedule%2F2020-2021-2-Bundesliga-Scores-and-Fixtures&div=div_sched_all"
url_BL1_2019_2020 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F20%2F3248%2Fschedule%2F2019-2020-Bundesliga-Scores-and-Fixtures&div=div_sched_all"
url_BL2_2019_2020 <- "https://widgets.sports-reference.com/wg.fcgi?css=1&site=fb&url=%2Fen%2Fcomps%2F33%2F3249%2Fschedule%2F2019-2020-2-Bundesliga-Scores-and-Fixtures&div=div_sched_all"
BL1_2021_2022 <- read_html(url_BL1_2021_2022) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]
BL2_2021_2022 <- read_html(url_BL2_2021_2022) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]
BL1_2020_2021 <- read_html(url_BL1_2020_2021) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]
BL2_2020_2021 <- read_html(url_BL2_2020_2021) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]
BL1_2019_2020 <- read_html(url_BL1_2019_2020) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]
BL2_2019_2020 <- read_html(url_BL2_2019_2020) %>%
  html_nodes('table') %>%
  html_table() %>%
  .[[1]]

dataset_Bundesliga <- BL1_2021_2022 %>%
  select(Date, Home, Away, Score) %>%
  rbind(BL2_2020_2021 %>%
          select(Date, Home, Away, Score)) %>%
  rbind(BL1_2019_2020 %>%
          select(Date, Home, Away, Score)) %>%
  rbind(BL2_2019_2020 %>%
          select(Date, Home, Away, Score)) %>%
  mutate(Home = gsub("ü", "ue", Home)) %>%   #leider im HTML Datensatz mit falscher Umschreibung
  mutate(Home = gsub("ö", "oe", Home)) %>%
  mutate(Away = gsub("ü", "ue", Away)) %>%
  mutate(Away = gsub("ö", "oe", Away)) %>%
  filter(!(Date=="")) %>%
  separate(Score, into=c("Home_Score", "Away_Score"), convert=TRUE) %>%
  mutate(Home_Score = as.numeric(Home_Score)) %>%
  mutate(Away_Score = as.numeric(Away_Score)) %>%
  mutate(Result = ifelse(Home_Score > Away_Score, "H",
                         ifelse(Home_Score < Away_Score, "A", "D"))) %>%
  mutate(year = format(as.Date(Date),"%Y")) %>%
  as.data.frame()
head(dataset_Bundesliga)

dataset_Bundesliga %>%
  group_by(year) %>%
  summarise(avg_home_goals = mean(Home_Score),
            avg_away_goals = mean(Away_Score))

dataset_Bundesliga %>%
  ggplot(aes()) +
  geom_density(aes(x=Home_Score,  fill="Heim"), adjust=8, alpha=0.5) +
  geom_density(aes(x=Away_Score,  fill="Gast"), adjust=8, alpha=0.5) +
  scale_x_continuous(breaks = 0:8) +
  theme_minimal() +
  scale_fill_manual(values=c("Heim" = "#327D8F", "Gast" = "#E0D556")) +
  labs(title = "geschossene Tore Heim- und Gastteam",
       x = "Tore", y = "Dichte")

# Trainings- & Testdatensatz
df_train <- dataset_Bundesliga %>%
  filter(Date < "2021-06-01" )
df_test <- dataset_Bundesliga %>%
  filter(Date >= "2021-06-01") %>%
  drop_na(Result)
# Datenaufbereitung
df_train_long <- rbind(
  df_train %>%
    select(team = Home, attack = Home_Score, defense = Away_Score) %>%
    mutate(type = "home"),
  df_train %>%
    select(team = Away, attack = Away_Score, defense = Home_Score) %>%
    mutate(type = "away")
  )
# Modellbildung
# die Durchschnittswerte der geschosenen Tore für den Angriff, die kassierten Tore für die Verteidigung
model1 <- df_train_long %>%
  group_by(team) %>%
  summarise(alpha=mean(attack), beta=mean(defense))
head(model1)

team_alphas <- model1$alpha %>% 'names<-'(model1$team)
team_betas <- model1$beta %>% 'names<-'(model1$team)
gamma <- sum(df_train$Home_Score) / sum(df_train$Away_Score) #for home advantage
prediction_model1 <- df_test %>%
  select(Date, Home, Away, Home_Score, Away_Score, Result) %>%
  mutate(Home_Score_e = round(team_alphas[Home]* team_betas[Away]* gamma,3)) %>%
  mutate(Away_Score_e = round(team_alphas[Away]* team_betas[Home],3)) %>%
  mutate(Result_e = ifelse(Home_Score_e > Away_Score_e, "H",
                           ifelse(Away_Score_e > Home_Score_e, "A", "D"))) %>%
  as.data.frame()
head(prediction_model1)

accuracy_model1 <- tibble(correct = prediction_model1 %>%
                            filter(Result == Result_e) %>% 
                            nrow(),
                          total = prediction_model1 %>%
                            nrow(),
                          accuracy = percent(correct / total, accuracy=0.01)) %>%
  as.data.frame() %>%
  print

table(actual=prediction_model1$Result, predicted = prediction_model1$Result_e)

dataset_poisson <- dataset_Bundesliga %>%
    drop_na(Result)
poisson <- bind_rows(list(
  merge(
    tibble(Goals = seq(0, max(dataset_poisson$Away_Score, dataset_poisson$Home_Score), by=1)) %>%
      mutate(pred=dpois(0:(nrow(.)-1), mean(dataset_poisson$Home_Score))) %>%
      mutate(type = "home"),
    dataset_poisson %>%
      group_by(Home_Score) %>%
      summarize(actual=n()/nrow(.)), 
    by.x="Goals", by.y="Home_Score", all.x=TRUE),
  merge(
    tibble(Goals = seq(0, max(dataset_poisson$Away_Score, dataset_poisson$Home_Score), by=1)) %>%
      mutate(pred=dpois(0:(nrow(.)-1), mean(dataset_poisson$Away_Score))) %>%
      mutate(type="away"),
    dataset_poisson %>%
      group_by(Away_Score) %>%
      summarize(actual=n()/nrow(.)), 
    by.x="Goals", by.y="Away_Score", all.x=TRUE))) %>%
  mutate(actual = if_else(is.na(actual), 0, actual))
poisson %>%
  ggplot(aes(x=as.factor(Goals))) + 
  geom_col(aes(y=actual, fill=type), stat="identity", position="dodge", alpha=0.5) +
  geom_line(aes(y=pred, group=type, color=type), size=1.25) +
  scale_fill_manual(values=c("#327D8F", "#E0D556"), name="actual") +
  scale_color_manual(values=c("#327D8F", "#E0D556"), name="Poisson") +
  theme_minimal() +
  labs(title = "Anteil der erzielten und geschätzten Tore pro Spiel",
       x = "erzielte Tore", y = "Anteil an Spielen")

df_freiburg.home <- dataset_poisson %>%
  filter(Home == "Freiburg")
df_freiburg.away <- dataset_poisson %>%
  filter(Away == "Freiburg")
poisson_freiburg <- bind_rows(list(
  merge(
    tibble(Goals = seq(0, max(df_freiburg.away$Away_Score, df_freiburg.home$Home_Score), by=1)) %>%
      mutate(pred=dpois(0:(nrow(.)-1), mean(df_freiburg.home$Home_Score))) %>%
      mutate(type = "home"),
    df_freiburg.home %>%
      group_by(Home_Score) %>%
      summarize(actual=n()/nrow(.)), 
    by.x="Goals", by.y="Home_Score", all.x=TRUE),
  merge(
    tibble(Goals = seq(0, max(df_freiburg.away$Away_Score, df_freiburg.home$Home_Score), by=1)) %>%
      mutate(pred=dpois(0:(nrow(.)-1), mean(df_freiburg.away$Away_Score))) %>%
      mutate(type="away"),
    df_freiburg.away %>%
      group_by(Away_Score) %>%
      summarize(actual=n()/nrow(.)), 
    by.x="Goals", by.y="Away_Score", all.x=TRUE))) %>%
  mutate(actual = if_else(is.na(actual), 0, actual))
poisson_freiburg %>%
  ggplot(aes(x=as.factor(Goals))) + 
  geom_col(aes(y=actual, fill=type), stat="identity", position="dodge", alpha=0.5) +
  geom_line(aes(y=pred, group=type, color=type), size=1.25) +
  scale_fill_manual(values=c("#327D8F", "#E0D556"), name="actual") +
  scale_color_manual(values=c("#327D8F", "#E0D556"), name="Poisson") +
  theme_minimal() +
  labs(title = "Anteil der von Freiburg erzielten und geschätzten Tore pro Spiel",
       x = "erzielte Tore", y = "Anteil an Spielen")

dskellam(1,mean(dataset_poisson$Home_Score),mean(dataset_poisson$Away_Score))

dataset_poisson %>%
  mutate(goal_diff = Home_Score-Away_Score) %>%
  group_by(goal_diff) %>%
  summarize(actual=n()/nrow(.)) %>% 
  inner_join(data.frame(goal_diff=-5:5,
                        pred=dskellam(-5:5, mean(df_train$Home_Score), mean(df_train$Away_Score))),
             by=c("goal_diff")) %>%
  ggplot(aes(x=as.factor(goal_diff))) + 
  geom_bar(aes(y = actual,fill="actual"), stat="identity") +
  geom_line(aes(y = pred, group = 1, color = "Skellam"), size=1.25) +
  scale_fill_manual(values=c("#327D8F"), name="actual") +
  scale_color_manual(values=c("#E0D556"), name="Skellam") +
  theme_minimal() +
  labs(title = "Tordifferenz Heim- vs Gast-Team",
       x = "Tordifferenz", y = "Anteil an Spielen")

poisson_model <- rbind(
  data.frame(goals=df_train$Home_Score,
             team=df_train$Home,
             opponent=df_train$Away,
             home=1),
  data.frame(goals=df_train$Away_Score,
             team=df_train$Away,
             opponent=df_train$Home,
             home=0)) %>%
  glm(goals ~ home + team +opponent, family=poisson(link=log),data=.)
summary(poisson_model)

simulate_match <- function(foot_model, Home, Away, max_goals=10){
  home_goals_avg <- predict(foot_model,
                            data.frame(home=1, team=Home, 
                                       opponent=Away), type="response")
  away_goals_avg <- predict(foot_model, 
                            data.frame(home=0, team=Away, 
                                       opponent=Home), type="response")
  dpois(0:max_goals, home_goals_avg) %o% dpois(0:max_goals, away_goals_avg) 
}
game_sim <- simulate_match(poisson_model, "Freiburg", "Stuttgart", max_goals=10)
library(scales)
percent(game_sim, accuracy = 0.001)

tibble(Freiburg_win = percent(sum(game_sim[lower.tri(game_sim)]), accuracy=0.1),
       Draw = percent(sum(diag(game_sim)), accuracy=0.1),
       Stuttgart_win = percent(sum(game_sim[upper.tri(game_sim)]), accuracy=0.1))

poisson_predictions <- df_test %>%
  filter(Home != "Hansa Rostock") %>%
  filter(Away != "Hansa Rostock")
for (i in 1:nrow(poisson_predictions)){
  sim <- simulate_match(poisson_model, poisson_predictions$Home[i], poisson_predictions$Away[i], max_goals=10)
  poisson_predictions$Sim_HomeWin[i]=sum(sim[lower.tri(sim)])
  poisson_predictions$Sim_Draw[i]=sum(diag(sim))
  poisson_predictions$Sim_AwayWin[i]=sum(sim[upper.tri(sim)])
}
poisson_predictions <-  poisson_predictions %>%
  rowwise() %>%
  mutate(Result_Sim = if_else(Sim_HomeWin == max(Sim_HomeWin, Sim_Draw, Sim_AwayWin), "H", 
                              if_else(Sim_AwayWin == max(Sim_HomeWin, Sim_Draw, Sim_AwayWin), "A",
                                      "D")))

accuracy_model1 <- tibble(correct = poisson_predictions %>%
                            filter(Result == Result_Sim) %>% 
                            nrow(),
                          total = poisson_predictions %>%
                            nrow(),
                          accuracy = correct / total) %>%
  as.data.frame() %>%
  print

table(actual=prediction_model1$Result, predicted = prediction_model1$Result_e)

poisson_model2 <- rbind(
  data.frame(goals=dataset_poisson$Home_Score,
             team=dataset_poisson$Home,
             opponent=dataset_poisson$Away,
             home=1),
  data.frame(goals=dataset_poisson$Away_Score,
             team=dataset_poisson$Away,
             opponent=dataset_poisson$Home,
             home=0)) %>%
  glm(goals ~ home + team +opponent, family=poisson(link=log),data=.)
poisson_predictions2 <- dataset_Bundesliga %>%
  filter(Date == "2022-01-07" | Date == "2022-01-08" | Date == "2022-01-09") %>%
  filter(Home != "Hansa Rostock") %>%
  filter(Away != "Hansa Rostock")
for (i in 1:nrow(poisson_predictions2)){
  sim <- simulate_match(poisson_model2, poisson_predictions2$Home[i], poisson_predictions2$Away[i], max_goals=10)
  poisson_predictions2$Sim_HomeWin[i]=sum(sim[lower.tri(sim)])
  poisson_predictions2$Sim_Draw[i]=sum(diag(sim))
  poisson_predictions2$Sim_AwayWin[i]=sum(sim[upper.tri(sim)])
  }
poisson_predictions2 %>%
  rowwise() %>%
  mutate(Sim_Result = if_else(Sim_HomeWin == max(Sim_HomeWin, Sim_Draw, Sim_AwayWin), "H", 
                              if_else(Sim_AwayWin == max(Sim_HomeWin, Sim_Draw, Sim_AwayWin), "A",
                                      "D"))) %>%
  select(Date, Home, Away, Sim_HomeWin, Sim_Draw, Sim_AwayWin, Sim_Result) %>%
  mutate(Sim_HomeWin = percent(as.numeric(Sim_HomeWin), accuracy=0.01)) %>%
  mutate(Sim_AwayWin = percent(as.numeric(Sim_AwayWin), accuracy=0.01)) %>%
  mutate(Sim_Draw = percent(as.numeric(Sim_Draw), accuracy=0.01)) %>%
  print(

Photo by Kelly L from Pexels

Beliebte Posts aus diesem Blog

Was ist fremd?

brandy74 "Malstunde" Some rights reserved. www.piqs.de Der Begriff Fremdheit wird benutzt zur Charakterisierung einer Beziehung. Immer muss etwas bekannt sein um es auch als fremd zu bezeichnen; andernfalls kann es nicht beschrieben werden. Wissenschaftlich wird die Fremdheit oft auch als die Gleichzeitigkeit von Nähe und Entferntheit, von Verbundenheit und Getrenntheit charakterisiert. Wer demnach etwas als fremd bezeichnet, unterscheidet die Welt an dieser Stelle in ein Innen und ein Außen. Das Fremde sei jenseits einer einer imaginären Grenze. Diese Grenzen können unterschiedlich lokalisiert werden. Bei der kulturellen Fremdheit werden andere kulturelle Verhaltensweisen und Ansichten identifiziert und als fremd bezeichnet. Bei der sozialen Fremdheit ist der Fremde hingegen Teil der eigenen Gesellschaft, der eigenen Gemeinschaft. Durch die Zuschreibung der sozialen Fremdheit wird er aus dem eigenen Bereich, also dem eigenen sozialen Milieu, exkludiert. Drückt sich

Sentiment-Analyse von deutschen Texten in R

Eine Sentiment-Analyse funktioniert im Grunde wiefolgt: die einzelnen Wörter werden eines Textes werden mit bestimmten Bibliotheken abgeglichen und eine Einteilung in "positiv/negativ" oder ein hinterlegter Sentiment-Wert abgegriffen. Die Summe dieser Werte ergibt schließlich den Sentiment-Score des ganzen Texts. Für englische Texte sind in R bereits Bibliotheken hinterlegt (z.B. im Package tidytext ). Für deutsche Texte habe ich auf meiner Recherche die Bibliothek  SentiWS  der Universität Leipzig gefunden. Die rund 16.000 positiven und 18.000 negativen Wörter sind mit einer Wertspanne von -1 zu 1 hinterlegt. Das Problem ist, dass diese in zwei Textdateien kommen, deren Format erst aufbereitet werden muss. So sieht die Bibliothek beim Einlesen aus: Mit folgendem Code habe ich mir die Bibliothek operationalisiert: library(dplyr) # SentiWS - Dateien hier runterladen: https://wortschatz.uni-leipzig.de/en/download # a) negative Wörter # die Textdatei einlesen negat

Migration und Bevölkerungsentwicklung: Solidarität und Selbsthilfe

Aus: Neue Potenziale - zur Lage der Nation in Deutschland , Juni 2014,  Berlin-Institut für Bevölkerung und Entwicklung Vor ein paar Wochen war ich auf einem sehr spannenden Vortrag am ifo-Institut in München von Herrn Dr. Klingholz, Direktor des Berlin Instituts für Bevölkerung und Entwicklung. Der Vortrag widmete sich einerseits der Zusammensetzung und dem Bildungs- wie Integrationsgrad deutscher Migranten und andererseits der zukünftigen Bevölkerungsentwicklung in Teilen der Welt und deren Auswirkungen auf die Migration in Europa, bzw. Deutschland. Polarisierend Unterteilt man die Migranten(1) nach Gruppen hinsichtlich ihrer Herkunftsländer, so zeigt sich oft eine starke Polarisierung des Bildungsgrades. Beispiel Rumänien und Polen. Zwar ist der Anteil der Migranten aus Rumänien und Polen ohne Bildungsabschluss wesentlich höher als der Anteil der Einheimischen. Umgekehrt ist der Anteil an Akademikern bei Migranten aus Rumänien und Polen höher als bei Einheimischen. Auch

die Hot-Dog-Ökonomie

Diego Torres Silvestre " Ice Creams, Hot Dogs & Pretzels" Some rights reserved. www.piqs.de Man stelle sich eine Wirtschaft vor, in der nur zwei Güter hergestellt würden: Würstchen und Brötchen. Konsumenten würden Hotdogs kaufen; also jeweils ein Brötchen mit einer Wurst. Die Fertigung geschieht durch Menschenhand. So fing Paul Krugman 1997 einen Artikel für das Online-Magazine Slate an, in welchem er den Zusammenhang von Technologie, Jobs und Kapitalismus erklären will. Er fährt fort, dass in dieser Wirtschaft 120 Millionen Arbeiter beschäftigt sind, was einer Vollbeschäftigung entspreche. Zur Herstellung einer Wurst oder eines Brötchens benötige es zwei Arbeitstage. Die 60 Millionen Angestellten in der Brötchenproduktion und genauso viele in der Wurstfabrikationen produzieren demnach täglich 30 Millionen Brötchen und Würste. Angenommen es komme eine verbesserte Technologie auf, mit deren Hilfe ein Arbeiter zur Herstellung einer Wurst nur noch einen Tag

die schöne Welt von Red Bull

Till Krech "wroooom" Some rights reserved. www.piqs.de Red Bull – vom Marktführer für Energiegetränke zum kommenden Medienimperium? Das Magazin Fast Company vergab in der Liste „The World´s 50 Most Innovative Companies“ den 29. Platz an Red Bull für genau diese Entwicklung. Gebündelt unter dem Dach der Red Bull Media House GmbH besitzt der Konzern mittlerweile verschiedene Medienbeteiligungen und Neugründungen. Kritiker bezeichnen es als eine gewaltige Marketingmaschine. Rund ein Drittel des Umsatzes wird für die Pflege des Marktauftritts ausgegeben. Eine firmeninterne Nachrichtenagentur sammelt Inhalte zu einen der vielen weltweiten aufsehenerregenden Red-Bull-Ereignisse, um sie externen Medien gebündelt und aufbereitet zur Verfügung zu stellen. Über eigene Medien werden die Konsumenten sogar direkt erreicht. Das 2007 gegründete Hochglanzmagazin "Red Bulletin" hat bereits eine Auflage von 5 Millionen Heften erreicht und wird mehrspraching in zwö

Verspargelung der Landschaft

FZ 18: "Mount Idarkopf" www.piqs.de Some Rights reserved. Vielleicht ist es, weil ich erst 22 Jahre alt bin. Vielleicht weil es bei meiner Heimatstadt schon seit mehr als zehn Jahren ein Windrad gibt. Aber das Argument einer Verspargelung der Landschaft durch Windräder zählt für mich nicht. Ich komme aus Baden-Württemberg. Insofern verfolgt mich das Argument der Verspargelung der Landschaft durch den ehemaligen baden-württembergischen Ministerpräsidenten Erwin Teufel fast genauso lange wie das Windrad vor meiner Haustür. Das Argument wird immer wieder von jenen hervorgebracht, welche gegen die Aufstellung von Windrädern sind. Die einen fürchten um die Landschaft, andere finden sie einfach nicht schön und noch andere bringen es nur als Vorwand. Besonders die Nähe zur Atomwirtschaft fällt einem bei der hießigen CDU auf. In Baden-Württemberg ist der Fall bei den Windrädern vielleicht ein bisschen spezieller. Wenn man hier die Windenergie effizient nutzen will, so