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.
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.
Für die Umsetzung des Vorgehens werden die Packages dplyr und ggplot2 benötigt.
library(dplyr)
library(ggplot2)
library(knitr)
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 |
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"
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")