Ziel

In letzter Zeit wird immer wieder der Vorwurf laut, dass die Bundesliga im Vergleich zu den weiteren europäischen Top-Ligen langweilig ist oder zumindest langweiliger geworden. In der Öffentlichkeit werden verschiedene Gründe für diese “gefühlte” Langeweile angeführt. Nachfolgend soll anhand einer grafischen Analyse überprüft werden, ob die Bundesliga tatsächlich im Vergleich zu den weiteren europäischen Top-Ligen in der Hinrunde der Saison 2015/2016 als langweilig einzustufen ist.

Als Kriterium für die Spannung innerhalb einer Liga wird in dieser Analyse die Häufigkeit von Tabellenplatzwechseln gesehen. Eine Liga ist umso spannender, je mehr Wechsel der Tabellenplätze stattfinden. Weitere mögliche und sinnvolle Kriterien (z.B. Punktedifferenz zwischen Tabellenplätze, usw.) werden in diesem Vorgehen nicht betrachtet.

Ergebnis

In den nachfolgenden Abbildungen wird das Ergebnis der Analysen dargestellt. Für jede Liga wurde eine Abbildung erstellt, auf deren x-Achse die Spieltage der Hinrunde 2015/2016 abgetragen sind. Auf der y-Achse ist der Tabellenplatz aufgeführt. Hat z.B. am 6. Spieltag in der Bundesliga der Tabellenführer gewechselt, so ist das dortige Kästchen dunkelblau (Wechsel Tabellenplatz = Ja). Ist dagegen am 7. Spieltag der Tabellenführerer konstant geblieben, so ist das dortige Kästchen grau. Je mehr dunkelblau eine Abbildung im gesamten Saisonverlauf in allen Tabellenregionen besitzt, desto spannender ist die Liga.

Die Abbildungen zeigen, dass in der Bundesliga insbesondere die oberen beiden Tabellenplätze konstant geblieben sind. Im Vergleich dazu gab es in der Premier League insbesondere in dem oberen Tabellenbereich häufigere Wechsel. Dies trifft auch auf die Primera Division und die Serie A zu. In diesen Tabellenbereichen sind somit die weiteren europäischen Ligen spannender als die Bundesliga.

In dem unteren Tabellendrittel sieht es dagegen (leicht) anders aus. Während es in der Bundesliga dort häufige Tabellenwechsel gab, finden in diesen Bereichen vor allem in der Serie A und der Premier League nicht so viele Wechsel statt. Hier ist die Bundesliga als spannender einzustufen.

Umsetzung mit dplyr und ggplot2

Für die Umsetzung des Vorgehens werden die Packages dplyr und ggplot2 benötigt.

library(dplyr)
library(ggplot2)
library(knitr)

Datenaufbereitung

Zuerst müssen die Rohdaten aufbereitet werden. Als Rohdaten dienen die Spieltagsergebnisse von https://github.com/openfootball/. Die Spieltagsergebnisse wurden für die Bundesliga, die Premier League, die Primera Division und die Serie A als csv-Datei gespeichert und in dem nachfolgenden Format aufbereitet.

Matchday Home Away Goals.Home Goals.Away
1 Bayern München Hamburger SV 5 0
1 1. FSV Mainz 05 FC Ingolstadt 04 0 1
1 SV Darmstadt 98 Hannover 96 2 2
1 Werder Bremen FC Schalke 04 0 3
1 FC Augsburg Hertha BSC 0 1
1 Bayer Leverkusen 1899 Hoffenheim 2 1

Die aufbereiteten Spieltagsergebnisse liegen für jede Liga in separaten csv-Dateien vor und müssen eingelesen werden.

Bundesliga.df <- read.csv("Data/Bundesliga.csv", sep=";", stringsAsFactors = FALSE)
PremierLeague.df <- read.csv("Data/PremierLeague.csv", sep=";", stringsAsFactors = FALSE)
PrimeraDivision.df <- read.csv("Data/PrimeraDivision.csv", sep=";", stringsAsFactors = FALSE)
SerieA.df <- read.csv("Data/SerieA.csv", sep=";", stringsAsFactors = FALSE)

Für die Weiterverarbeitung der Spieltagsergebnisse in R dient die Funktion structeringResultData. Die Funktion hat das Ziel für jede Mannschaft, zu jedem Spieltag eine Zeile in einem Dataframe zu erzeugen, die die Informationen zu erzielten Tore und kassierten Gegentoren am Spieltag enthält. Entsprechend lässt sich aus dem Dataframe ermitteln, wie viele Punkte, Tore und Gegentore ein Team pro Spieltg erreicht hat.

structeringResultData <- function(Results.df){
  Home.df <- data.frame(Location=rep("Home",dim(Results.df)[1]))
  Away.df <- data.frame(Location=rep("Away",dim(Results.df)[1]))
  
  Home.df$Matchday <- Results.df$Matchday
  Home.df$Team <- Results.df$Home
  Home.df$Goals.For <- Results.df$Goals.Home
  Home.df$Goals.Against <- Results.df$Goals.Away
  
  Away.df$Matchday <- Results.df$Matchday
  Away.df$Team <- Results.df$Away
  Away.df$Goals.For <- Results.df$Goals.Away
  Away.df$Goals.Against <- Results.df$Goals.Home
  
  League.df <- rbind(Home.df, Away.df)
  
  League.df$Result <- ifelse(League.df$Goals.For > League.df$Goals.Against, "W",
                             ifelse(League.df$Goals.For < League.df$Goals.Against, "L",
                                    "D"))
  
  League.df$Points <- ifelse(League.df$Result == "W", 3,
                             ifelse(League.df$Result == "D", 1,
                                    0))
  
  League.df$CountWin <- ifelse(League.df$Result == "W", 1, 0)
  League.df$CountLost <- ifelse(League.df$Result == "L", 1, 0)
  League.df$CountDrawn <- ifelse(League.df$Result == "D", 1, 0)
  return(League.df)
}

Die Rohdaten besitzen nun - exemplarisch für die Bundesliaga - nach Anwendung der Funktion structeringResultData folgende Struktur:

Location Matchday Team Goals.For Goals.Against Result Points CountWin CountLost CountDrawn
Home 1 Bayern München 5 0 W 3 1 0 0
Home 1 1. FSV Mainz 05 0 1 L 0 0 1 0
Home 1 SV Darmstadt 98 2 2 D 1 0 0 1
Home 1 Werder Bremen 0 3 L 0 0 1 0
Home 1 FC Augsburg 0 1 L 0 0 1 0
Home 1 Bayer Leverkusen 2 1 W 3 1 0 0

Funktionen für die Erstellung einer Tabelle zu einem Spieltag mit dplyr

Aus Daten mit diesem Format lässt sich nun mit Hilfe des dplyr-Packages eine Tabelle erzeugen. Neben den Rohdaten in dem oben erwähnten Format League.df ist als weitere Information der Spieltag notwendig, für den die Tabelle erstellt werden soll. Die Information muss in der Variable Played an die Funktion übergeben werden. Mit der Funktion createLeagueTable werden die Punkte, Tore und Gegentore summiert. Ebenfalls wird die Tordifferenz bestimmt. Zum Schluss wird die Tabelle - nach vereinfachten Kriterien - sortiert und die Zeilennamen als Tabellenplatz dem Dataframe hinzugefügt.

createLeagueTable <- function(Played, League.df){
 
  LeagueTable.df <- League.df %>% filter(Matchday <= Played) %>% 
                                  group_by(Team)  %>% 
                                  summarise(P = Played,
                                            W = sum(CountWin),
                                            D = sum(CountDrawn),
                                            L = sum(CountLost),
                                            GF = sum(Goals.For),
                                            GA = sum(Goals.Against),
                                            GD = GF - GA,
                                            PTS = sum(Points))  %>% 
                                  arrange(desc(PTS), desc(GD), desc(GF), Team)
    
  
  LeagueTable.df <- add_rownames(LeagueTable.df, var = "POS")
  return(LeagueTable.df)
}

Für den 6. Spieltag der Bundesliga ergibt sich somit mit der oben beschriebenen Funktion die nachfolgende Tabelle.

POS Team P W D L GF GA GD PTS
1 Bayern München 6 6 0 0 20 3 17 18
2 Borussia Dortmund 6 5 1 0 19 4 15 16
3 FC Schalke 04 6 4 1 1 9 5 4 13
4 VfL Wolfsburg 6 3 2 1 9 7 2 11
5 Hertha BSC 6 3 1 2 7 7 0 10
6 Hamburger SV 6 3 1 2 8 9 -1 10
7 1. FC Köln 6 3 1 2 9 11 -2 10
8 FC Ingolstadt 04 6 3 1 2 3 5 -2 10
9 1. FSV Mainz 05 6 3 0 3 9 6 3 9
10 SV Darmstadt 98 6 2 3 1 6 7 -1 9
11 Bayer Leverkusen 6 3 0 3 4 8 -4 9
12 Eintracht Frankfurt 6 2 2 2 12 8 4 8
13 Werder Bremen 6 2 1 3 7 9 -2 7
14 FC Augsburg 6 1 1 4 6 9 -3 4
15 VfB Stuttgart 6 1 0 5 8 14 -6 3
16 Bor. Mönchengladbach 6 1 0 5 6 14 -8 3
17 1899 Hoffenheim 6 0 2 4 5 11 -6 2
18 Hannover 96 6 0 1 5 5 15 -10 1

Es fehlt allerdings noch die Information, welchen Tabellenplatz das Team an dem vorherigen Stichtag hatte. Diese Information wird über die Funktion createMatchdayData hinzugefügt. Die Funktion ruft die obige Funktion für den aktuellen und den vorherigen Stichtag auf und führt die Ergebnisse zusammen. Der Tabellenplatz zu dem vorherigen Stichtag ist in der Spalte LP gespeichert.

createMatchdayData <- function(Played, League.df){
  
  Current.LeagueTable <- createLeagueTable(Played, League.df = League.df)

  if(Played-1==0){
    Last.LeagueTable <- data.frame(Team = Current.LeagueTable$Team,
                                   LP = rep(0,length(Current.LeagueTable$Team)))
    Last.LeagueTable$Team <- as.character(Last.LeagueTable$Team)
  } else {
    Last.LeagueTable <- createLeagueTable(Played-1, League.df = League.df)
    Last.LeagueTable <- Last.LeagueTable %>%
                        select(Team, LP = POS)
  }

  Matchday.Data <- left_join(Current.LeagueTable,Last.LeagueTable, by="Team")
  Matchday.Data <- Matchday.Data %>%
                          select(P, POS, Team, LP, PTS)
  return(Matchday.Data)
}

Die Funktion createLeagueData bedient sich der zwei oben dargestellten Funktionen. Als Inputfaktoren dienen die aufbereiten Rohdaten und die Anzahl der betrachten Spieltage. Zu jedem Spieltag wird mit Hilfe der Funktion eine Tabelle inkl. vorherigem Tabellenplatz erstellt und in eine gesamte Datenbasis übertragen.

createLeagueData <- function(League.df, Matchdays){

  Matchday.temp <- createMatchdayData(Played=1, League.df)
  LeagueData <- Matchday.temp
  
  for(i in 2:Matchdays){
    
    Matchday.temp <- createMatchdayData(Played=i, League.df)
    LeagueData <- rbind(LeagueData, Matchday.temp)
    
  }
  
  LeagueData$POS <- as.integer(LeagueData$POS)
  LeagueData$LP <- as.integer(LeagueData$LP)
  
  LeagueData$POSChange <- ifelse(LeagueData$POS - LeagueData$LP==0,"NO","YES")
  LeagueData$POSChange <- as.factor(LeagueData$POSChange)
  
  return(LeagueData)
}

Für die Bundesliga ergibt sich folgender Auszug aus dem Dataframe für die untere Tabellenregion des Spieltags 11.

P POS Team LP PTS POSChange
11 13 SV Darmstadt 98 11 13 YES
11 14 Hannover 96 15 11 YES
11 15 VfB Stuttgart 16 10 YES
11 16 Werder Bremen 14 10 YES
11 17 1899 Hoffenheim 17 7 NO
11 18 FC Augsburg 18 6 NO

Zusammendfassend lassen sich die aufbereiteten Dataframes für die vier europäischen Top-Ligen mit folgenden Befehlen ermitteln.

BuLi.Data <- createLeagueData(structeringResultData(Bundesliga.df), Matchdays=17)
BuLi.Data$League <- "Bundesliga"

PrLe.Data <- createLeagueData(structeringResultData(PremierLeague.df), Matchdays=19)
PrLe.Data$League <- "Premier League"

PrDiv.Data <- createLeagueData(structeringResultData(PrimeraDivision.df), Matchdays=19)
PrDiv.Data$League <- "Primeria Division"

SerieA.Data <- createLeagueData(structeringResultData(SerieA.df), Matchdays=19)
SerieA.Data$League <- "Serie A"

Erstellung der Abbildungen mit ggplot2

Die aufbereiten Dataframes besitzen eine Struktur, die direkt mit ggplot2 zu einer Abbildung weiter verarbeitet werden kann. Hierfür dient die Funktion makePositionChangePlot.

makePositionChangePlot <- function(plot.data, days, teams, league){
  g <- ggplot(plot.data, aes(x=P,y=POS))
  g <- g + geom_tile(aes(fill = POSChange), colour = "black")
  g <- g + scale_x_continuous("Spieltag", breaks = seq(1:days)) + scale_y_reverse("Tabellenplatz", breaks = seq(1:teams))
  g <- g + scale_fill_manual(values = c("grey","darkblue")) + guides(fill=guide_legend(title="Wechsel Tabellenplatz", override.aes = list(colour = NULL))) + myMapTheme 
  g <- g + ggtitle(league)
  return(g)
}

Mit dieser Funktion lassen sich anschließend für jede der vier Ligen die oben bereits gezeigten Abbildungen erzeugen.

buli.plot <- makePositionChangePlot(plot.data = BuLi.Data, days=17, teams=18, league="Bundesliga")
prle.plot <- makePositionChangePlot(plot.data = PrLe.Data, days=19, teams=20, league="Premier League")
prdiv.plot <- makePositionChangePlot(plot.data = PrDiv.Data, days=19, teams=20, league="Primera Division")
sere.plot <- makePositionChangePlot(plot.data = SerieA.Data, days=19, teams=20, league="Serie A")