Hausarbeit 3

Poisson-Regression und Saison-Simulation in R

Aufgaben (10 Punkte)

In dieser dritten Hausarbeit verbinden Sie Modellierung und Simulation in R. Grundlage sind reale Ergebnisse der Bundesliga-Saison 2024/25. Ziel ist es, aus beobachteten Toren ein einfaches Poisson-Modell zu schätzen und den festen Saisonspielplan anschließend viele Male neu zu simulieren.

Die Aufgabe ist bewusst so aufgebaut, dass Sie vor allem diese Kette nachvollziehen:

  • Modell schätzen
  • erwartete Tore berechnen
  • daraus mit rpois() konkrete Spielergebnisse simulieren
  • die komplette Saison oft wiederholen
  • die resultierenden Wahrscheinlichkeiten auswerten und visualisieren
WichtigArbeitsweise

Sie dürfen für diese Hausarbeit bei Bedarf zusätzliche Pakete wie ggplot2 für Visualisierungen einsetzen.

Die beiden unten angegebenen Hilfsblöcke

  • zur Erzeugung des Long-Formats und
  • zur Berechnung der Abschlusstabelle

sind bereitgestellt und nicht Teil der eigentlichen Prüfungsleistung. Sie dürfen diese Blöcke unverändert in Ihre Abgabe übernehmen.

Bereitgestellte Daten und Hilfsblöcke

Verwenden Sie für diese Hausarbeit die bereitgestellte Saison-Datei bundesliga_2024_25.csv. Die Datei enthält genau diese Spalten:

  • matchday
  • date
  • home_team
  • away_team
  • home_goals
  • away_goals

Bereitgestellter Code: Long-Format

Der folgende Code erzeugt aus der Match-Tabelle das für das Modell benötigte Long-Format. Jede Partie wird dabei in zwei Zeilen dargestellt: einmal aus Sicht des Heimteams und einmal aus Sicht des Auswärtsteams.

Bereitgestellten Code anzeigen
library(readr)
matches <- read_csv("/home/share/datascience/data/bundesliga_2024_25.csv")
matches$match_id <- seq_len(nrow(matches))

matches_long <- rbind(
  data.frame(
    match_id = matches$match_id,
    matchday = matches$matchday,
    team = matches$home_team,
    opponent = matches$away_team,
    home = 1L,
    goals = matches$home_goals,
    stringsAsFactors = FALSE
  ),
  data.frame(
    match_id = matches$match_id,
    matchday = matches$matchday,
    team = matches$away_team,
    opponent = matches$home_team,
    home = 0L,
    goals = matches$away_goals,
    stringsAsFactors = FALSE
  )
)

matches_long$team <- factor(matches_long$team)
matches_long$opponent <- factor(matches_long$opponent)
HinweisBeispiel: matches_long

Die Tabelle matches_long enthält pro Spiel zwei Zeilen, also je eine Zeile aus Sicht des Heimteams und des Auswärtsteams.

match_id matchday team opponent home goals
1 1 Borussia Mönchengladbach Bayer Leverkusen 1 2
1 1 Bayer Leverkusen Borussia Mönchengladbach 0 3

So erkennt man direkt:

  • home = 1 bedeutet Heimteam-Sicht,
  • home = 0 bedeutet Auswärtsteam-Sicht,
  • goals ist immer die Torzahl des Teams in der jeweiligen Zeile.

1. Teil: Daten und Modell (2 Punkte)

Laden Sie die Saison-Datei, führen Sie den bereitgestellten Code zum Long-Format aus und schätzen Sie anschließend das folgende Poisson-Modell:

glm(goals ~ home + team + opponent, family = poisson(link = "log"), data = matches_long)

Dieses Modell beschreibt die erwartete Toranzahl eines Teams in Abhängigkeit davon,

  • ob das Team zuhause spielt (home),
  • welches Team angreift (team) und
  • gegen welches Team gespielt wird (opponent).

Ihre Aufgaben in diesem Teil:

  • schätzen Sie das Modell,
  • geben Sie die wichtigsten Modellinformationen aus,
  • erläutern Sie in wenigen Sätzen, was das Modell grundsätzlich beschreibt.

Es ist nicht erforderlich, jede einzelne Team-Koeffizientenschätzung im Detail zu interpretieren.

2. Teil: Erwartete Tore aus dem Modell (2 Punkte)

Berechnen Sie nun für alle Saisonspiele die erwarteten Heim- und Auswärtstore.

Dazu sollen Sie für jede Partie zwei Werte bestimmen:

  • lambda_home: erwartete Tore des Heimteams
  • lambda_away: erwartete Tore des Auswärtsteams

Erstellen Sie dafür eine Ergebnistabelle fixtures_with_lambda mit mindestens diesen Spalten:

  • matchday
  • home_team
  • away_team
  • lambda_home
  • lambda_away

Ihre Aufgaben in diesem Teil:

  • berechnen Sie lambda_home und lambda_away mit predict(..., type = "response"),
  • geben Sie einen kleinen Auszug der Tabelle aus,
  • interpretieren Sie 2 bis 3 konkrete Spiele kurz.
TippHinweise zu Teil 2

Für diesen Schritt ist vor allem wichtig:

  • Sie benötigen für jedes Spiel zwei Vorhersagen.
  • Für die Heimvorhersage setzen Sie home = 1 und verwenden team = home_team sowie opponent = away_team.
  • Für die Auswärtsvorhersage setzen Sie home = 0 und verwenden team = away_team sowie opponent = home_team.
  • Verwenden Sie bei predict() unbedingt type = "response", da Sie den erwarteten Torwert lambda benötigen und nicht den linearen Prädiktor auf der Log-Skala.
  • Eine saubere Lösung erzeugt zuerst eine kleine Tabelle mit den Spielpaarungen und ergänzt danach lambda_home und lambda_away.

Falls Sie diesen Zwischenschritt stärker führen möchten, dürfen Sie auch die folgende Hilfsfunktion unverändert verwenden. Diese Funktion ist nicht Teil der Prüfungsleistung.

Hilfsfunktion für erwartete Tore anzeigen
make_fixtures_with_lambda <- function(fit_goals, matches) {
  fixtures_with_lambda <- matches[
    ,
    c("matchday", "date", "home_team", "away_team")
  ]

  team_levels <- fit_goals$xlevels$team
  opponent_levels <- fit_goals$xlevels$opponent

  newdata_home <- data.frame(
    home = 1L,
    team = factor(matches$home_team, levels = team_levels),
    opponent = factor(matches$away_team, levels = opponent_levels)
  )

  newdata_away <- data.frame(
    home = 0L,
    team = factor(matches$away_team, levels = team_levels),
    opponent = factor(matches$home_team, levels = opponent_levels)
  )

  fixtures_with_lambda$lambda_home <- predict(
    fit_goals,
    newdata = newdata_home,
    type = "response"
  )

  fixtures_with_lambda$lambda_away <- predict(
    fit_goals,
    newdata = newdata_away,
    type = "response"
  )

  return(fixtures_with_lambda)
}
HinweisBeispiel: fixtures_with_lambda

Die folgende Tabelle zeigt beispielhaft, wie fixtures_with_lambda nach der Berechnung der erwarteten Tore aussehen kann:

matchday home_team away_team lambda_home lambda_away
1 Borussia Mönchengladbach Bayer Leverkusen 1.39 2.16
1 FC Augsburg Werder Bremen 1.14 1.44
1 SC Freiburg VfB Stuttgart 1.51 1.79

Dabei gilt:

  • lambda_home ist der erwartete Torwert des Heimteams,
  • lambda_away ist der erwartete Torwert des Auswärtsteams.

Vom Modell zur Saison-Simulation

In diesem Abschnitt geht es noch nicht um zusätzlichen Code, sondern um das Verständnis des Übergangs vom Modell zur Simulation.

Die Logik ist in dieser Hausarbeit genau wie folgt:

  1. Das geschätzte Poisson-Modell liefert für jede Team-Sicht auf ein Spiel einen erwarteten Torwert lambda.
  2. Dieser Wert lambda ist noch kein Spielergebnis, sondern der Mittelwert einer Poisson-Verteilung.
  3. Mit rpois(1, lambda) wird daraus eine mögliche Torzahl simuliert.
  4. Für jedes Saisonspiel werden Heim- und Auswärtstore getrennt simuliert: einmal mit lambda_home und einmal mit lambda_away.
  5. Aus diesen beiden simulierten Torzahlen entsteht ein simuliertes Endergebnis.
  6. Aus allen simulierten Endergebnissen einer Saison entsteht anschließend eine simulierte Abschlusstabelle.

Wichtig: In dieser Hausarbeit wird nicht der Spielplan neu erzeugt. Die Paarungen bleiben fest. Simuliert werden nur die Ergebnisse des bereits bekannten Spielplans.

HinweisKleines Beispiel

Angenommen, für ein Spiel ergeben sich

  • lambda_home = 1.8
  • lambda_away = 1.1

Dann könnte eine mögliche Simulation z. B. 2:0, 1:1 oder 1:2 ergeben. Alle diese Ergebnisse sind denkbare Realisationen auf Basis derselben erwarteten Torwerte.

3. Teil: Saison-Simulation (3 Punkte)

Schreiben Sie nun eine Funktion simulate_season(fixtures_with_lambda) oder äquivalenten Code, der eine komplette Saison einmal simuliert.

Dabei soll für jedes Spiel

  • ein Wert sim_home_goals aus rpois(1, lambda_home) und
  • ein Wert sim_away_goals aus rpois(1, lambda_away)

erzeugt werden.

Die Funktion bzw. der Code soll ein Ergebnisobjekt zurückgeben, das mindestens diese Spalten enthält:

  • matchday
  • home_team
  • away_team
  • sim_home_goals
  • sim_away_goals

Anschließend sollen Sie:

  • die bereitgestellte Funktion build_league_table() auf die simulierten Ergebnisse anwenden,
  • die komplette Saison mindestens 1000 Mal simulieren,
  • vor der Wiederholung mit set.seed(...) für Reproduzierbarkeit sorgen.

Da build_league_table() erst in diesem Teil benötigt wird, ist sie hier noch einmal an der passenden Stelle aufgeführt. Diese Funktion ist bereitgestellt und nicht Teil der Prüfungsleistung.

Hilfsfunktion anzeigen
build_league_table <- function(results_df) {
  teams <- sort(unique(c(results_df$home_team, results_df$away_team)))

  table_df <- data.frame(
    team = teams,
    played = 0L,
    wins = 0L,
    draws = 0L,
    losses = 0L,
    goals_for = 0L,
    goals_against = 0L,
    goal_diff = 0L,
    points = 0L,
    stringsAsFactors = FALSE
  )

  for (i in seq_len(nrow(results_df))) {
    home_team <- results_df$home_team[i]
    away_team <- results_df$away_team[i]
    home_goals <- results_df$sim_home_goals[i]
    away_goals <- results_df$sim_away_goals[i]

    home_idx <- match(home_team, table_df$team)
    away_idx <- match(away_team, table_df$team)

    table_df$played[home_idx] <- table_df$played[home_idx] + 1L
    table_df$played[away_idx] <- table_df$played[away_idx] + 1L

    table_df$goals_for[home_idx] <- table_df$goals_for[home_idx] + home_goals
    table_df$goals_against[home_idx] <- table_df$goals_against[home_idx] + away_goals
    table_df$goals_for[away_idx] <- table_df$goals_for[away_idx] + away_goals
    table_df$goals_against[away_idx] <- table_df$goals_against[away_idx] + home_goals

    if (home_goals > away_goals) {
      table_df$wins[home_idx] <- table_df$wins[home_idx] + 1L
      table_df$losses[away_idx] <- table_df$losses[away_idx] + 1L
      table_df$points[home_idx] <- table_df$points[home_idx] + 3L
    } else if (home_goals < away_goals) {
      table_df$wins[away_idx] <- table_df$wins[away_idx] + 1L
      table_df$losses[home_idx] <- table_df$losses[home_idx] + 1L
      table_df$points[away_idx] <- table_df$points[away_idx] + 3L
    } else {
      table_df$draws[home_idx] <- table_df$draws[home_idx] + 1L
      table_df$draws[away_idx] <- table_df$draws[away_idx] + 1L
      table_df$points[home_idx] <- table_df$points[home_idx] + 1L
      table_df$points[away_idx] <- table_df$points[away_idx] + 1L
    }
  }

  table_df$goal_diff <- table_df$goals_for - table_df$goals_against

  table_df <- table_df[
    order(
      -table_df$points,
      -table_df$goal_diff,
      -table_df$goals_for,
      table_df$team
    ),
  ]

  table_df$position <- seq_len(nrow(table_df))
  rownames(table_df) <- NULL

  table_df <- table_df[
    ,
    c("position", "team", "points", "goal_diff")
  ]

  return(table_df)
}
HinweisBeispiel: results_df

Nach einer Simulation könnte die Ergebnistabelle results_df z. B. so aussehen:

matchday home_team away_team sim_home_goals sim_away_goals
1 Borussia Mönchengladbach Bayer Leverkusen 1 1
1 FC Augsburg Werder Bremen 2 0
1 SC Freiburg VfB Stuttgart 1 1

Wichtig: Diese Werte sind simulierte Torzahlen. Bei einer anderen Simulation können hier andere Werte stehen.

HinweisBeispiel: simulierte Abschlusstabelle

Wendet man build_league_table(results_df) auf eine simulierte Saison an, kann die resultierende Tabelle z. B. so beginnen:

position team points goal_diff
1 Bayern München 78 68
2 Bayer Leverkusen 71 40
3 Eintracht Frankfurt 69 19
4 Mainz 05 63 33

Genau diese Art von Tabelle wird später über viele Simulationen hinweg ausgewertet.

4. Teil: Auswertung und Visualisierung (3 Punkte)

Werten Sie die vielen simulierten Saisons aus.

Berechnen Sie mindestens:

  • die Meisterwahrscheinlichkeit jedes Teams,
  • die mittlere Punktzahl jedes Teams über alle Simulationen.
HinweisTipp zur Sammlung der 1000 Simulationen

Ein für diese Aufgabe gut nachvollziehbarer Weg ist:

  • Legen Sie eine Matrix für die Punkte an: eine Zeile pro Simulation, eine Spalte pro Team.
  • Legen Sie eine zweite Matrix für die Tabellenpositionen an: ebenfalls eine Zeile pro Simulation und eine Spalte pro Team.
  • Füllen Sie beide Matrizen in einer Schleife, nachdem Sie für einen Simulationslauf die Abschlusstabelle berechnet haben.

Ein mögliches Grundgerüst sieht so aus:

teams <- sort(unique(fixtures_with_lambda$home_team))
n_teams <- length(teams)

points_mat <- matrix(NA_real_, nrow = n_sim, ncol = n_teams)
position_mat <- matrix(NA_integer_, nrow = n_sim, ncol = n_teams)
colnames(points_mat) <- teams
colnames(position_mat) <- teams

for (sim in seq_len(n_sim)) {
  simulated_results <- simulate_season(fixtures_with_lambda)
  season_table <- build_league_table(simulated_results)

  points_mat[sim, season_table$team] <- season_table$points
  position_mat[sim, season_table$team] <- season_table$position
}

Aus diesen beiden Matrizen lassen sich die Kennzahlen direkt berechnen:

  • Anzahl Meisterschaften je Team: Zählen Sie, wie oft in position_mat der Wert 1 vorkommt.
  • Meisterwahrscheinlichkeit je Team: Bilden Sie den Mittelwert von position_mat == 1.
  • Mittlere Punktzahl je Team: Bilden Sie die Spaltenmittelwerte von points_mat.
  • Boxplots je Team: Verwenden Sie die Werte aus points_mat.

Zum Beispiel:

champion_count <- colSums(position_mat == 1)
champion_probability <- colMeans(position_mat == 1)
mean_points <- colMeans(points_mat)

Erstellen Sie außerdem zwei Visualisierungen:

  • ein Balkendiagramm der Meisterwahrscheinlichkeiten aller Teams,
  • einen Boxplot je Team zum Vergleich der simulierten Punkteverteilungen.

Arbeiten Sie die wichtigsten Ergebnisse in wenigen Sätzen heraus und benennen Sie zum Schluss kurz die Grenzen des verwendeten Modells. Gehen Sie dabei z. B. auf folgende Vereinfachungen ein:

  • Heim- und Auswärtstore werden unabhängig simuliert,
  • der Heimvorteil ist konstant,
  • Form, Verletzungen oder Saisonverlauf werden nicht berücksichtigt.

Abgabe

Abgabe des Codes inklusive kurzer Erläuterungen, Visualisierungen und Interpretation als Quarto-Skript (.qmd) und gerendertes HTML über Teams.

Bitte nutzen Sie für die Abgabe die Vorlage Abgabe_Hausaufgabe3_ab12345.qmd. Die Saison-Datei steht ebenfalls direkt zum Download bereit: bundesliga_2024_25.csv.

Bitte ersetzen Sie im Dateinamen ab12345 durch Ihre FH-Kennung und tragen Sie im Dokument Ihren Namen und Ihre Matrikelnummer ein.

Die beiden bereitgestellten Hilfsblöcke zum Long-Format und zur Abschlusstabelle dürfen Sie unverändert übernehmen. Bewertet werden vor allem:

  • der korrekte Einsatz des Poisson-Modells,
  • die saubere Berechnung der erwarteten Tore,
  • die korrekte Saison-Simulation,
  • die Auswertung der Simulationen,
  • die Qualität von Visualisierung, Interpretation und Dokumentation.

Abgabefrist: 20. April 2026, 23:59 Uhr

Bewertungskriterien

Tabelle 1: Bewertungskriterien zur Hausarbeit 3
Kriterium Gewicht ausgezeichnet (30) gut (25) akzeptabel (20) verbesserungswürdig (15) inakzeptabel (0)
Modell und erwartete Tore 40 % Das Poisson-Modell ist korrekt geschätzt; die erwarteten Heim- und Auswärtstore sind korrekt berechnet, sauber dokumentiert und nachvollziehbar interpretiert. Das Poisson-Modell ist korrekt geschätzt; die erwarteten Tore sind weitgehend korrekt berechnet, mit kleineren Schwächen in Dokumentation oder Interpretation. Modell und erwartete Tore sind größtenteils korrekt, weisen aber kleinere Schwächen in Logik, Struktur oder Interpretation auf. Modell oder erwartete Tore sind nur teilweise korrekt oder unklar umgesetzt. Das Modell ist nicht sinnvoll umgesetzt oder die erwarteten Tore fehlen bzw. sind nicht nachvollziehbar.
Saison-Simulation 30 % Die Saison-Simulation ist korrekt implementiert, reproduzierbar und nutzt die geschätzten Erwartungswerte konsistent zur Erzeugung der Spielergebnisse und Tabellen. Die Saison-Simulation ist weitgehend korrekt implementiert und nutzt die Erwartungswerte im Wesentlichen richtig. Die Saison-Simulation funktioniert im Grundsatz, weist aber kleinere Fehler oder Unschärfen in der Umsetzung auf. Die Saison-Simulation ist nur teilweise korrekt oder in zentralen Teilen fehlerhaft. Die Saison-Simulation fehlt oder ist in wesentlichen Teilen nicht funktionsfähig.
Auswertung, Visualisierung und Interpretation 30 % Meisterwahrscheinlichkeiten und mittlere Punktzahlen sind korrekt ausgewertet; die Visualisierungen sind passend und die Ergebnisse werden fundiert interpretiert. Die Auswertung ist korrekt, die Visualisierungen sind sinnvoll und die Interpretation ist weitgehend überzeugend. Auswertung und Visualisierungen sind im Wesentlichen vorhanden, zeigen aber Schwächen in Genauigkeit, Lesbarkeit oder Interpretation. Auswertung, Visualisierung oder Interpretation sind nur teilweise gelungen und lassen wichtige Punkte offen. Es fehlt eine sinnvolle Auswertung oder die Ergebnisse werden nicht nachvollziehbar visualisiert und interpretiert.
Zurück nach oben