Kapitel 4. Multiple Regression: Rushing Yards über Erwartung

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Im vorigen Kapitel hast du die Rushing Yards durch eine andere Brille betrachtet, indem du die Anzahl der Yards kontrolliert hast, die für ein First Down oder einen Touchdown benötigt werden. Du hast festgestellt, dass der gesunde Menschenverstand überwiegt: Je mehr Yards ein Team für ein First Down oder einen Touchdown braucht, desto einfacher ist es, Yards auf dem Boden zu erzielen. Das zeigt dir, dass es wichtig ist, das Spiel der Running Backs zu verstehen, wenn du dich auf solche Dinge einstellst.

Eine Einschränkung der einfachen linearen Regression ist, dass es im Laufspiel mehr als eine wichtige Variable gibt, die angepasst werden muss. Während die Entfernung zum First Down oder Touchdown eine große Rolle spielt, ist vielleicht auch das Down wichtig. Es ist wahrscheinlicher, dass ein Team den Ball beim First Down und 10 Yards vor dem Touchdown laufen lässt, als beim Third Down und 10 Yards vor dem Touchdown, so dass die Verteidigung eher darauf vorbereitet ist, den Lauf beim First Down zu stoppen als beim Third Down.

Ein weiteres Beispiel ist die Punktedifferenz. Der Spielstand beeinflusst die Erwartungshaltung in mehrfacher Hinsicht, denn eine Mannschaft, die mit vielen Punkten Vorsprung führt, wird die Line of Scrimmage nicht so stark beanspruchen wie eine Mannschaft, die sich in einem engen Spiel mit ihrem Gegner befindet. Generell müssen bei der Bewertung eines Fußballspiels eine Vielzahl von Variablen berücksichtigt werden. Das machen wir mit Hilfe der multiplen linearen Regression.

Definition der multiplen linearen Regression

Wir wissen, dass das Laufen des Fußballs nicht nur von einer Sache beeinflusst wird. Deshalb müssen wir ein Modell erstellen, das Rushing Yards vorhersagt, aber weitere Merkmale enthält, um andere Faktoren zu berücksichtigen, die die Vorhersage beeinflussen könnten. Um auf Kapitel 3 aufzubauen, brauchen wir also eine multiple Regression.

Diemultiple Regression schätzt die Auswirkung mehrerer(multipler) Prädiktoren auf eine einzelne Antwort, indem sie eine lineare Kombination (oder Regression) der Prädiktorvariablen verwendet. In Kapitel 3 wurde die einfache lineare Regression vorgestellt, die ein Spezialfall der multiplen Regression ist. Bei einer einfachen linearen Regression gibt es zwei Parameter: einen Achsenabschnitt (oder Durchschnittswert) und eine Steigung. Diese modellieren die Wirkung eines kontinuierlichen Prädiktors auf die Antwort. In Kapitel 3 hat die Python/R-Formel für deine einfache lineare Regression die Rushing Yards durch einen Intercept und die Yards to go vorhergesagt: rushing_yards ~ 1 + ydstogo.

Es kann jedoch sein, dass du an mehreren Prädiktoren interessiert bist. Betrachte zum Beispiel die multiple Regression, die bei der Schätzung der erwarteten Rushing Yards den Down korrigiert. Du würdest eine Gleichung (oder Formel) verwenden, die Rushing Yards auf der Grundlage von Yards to go und Down vorhersagt: rushing_yards ~ ydstogo + down. Yards to go ist ein Beispiel für eine kontinuierliche Prädiktorvariable. Inoffiziell kann man sich kontinuierliche Prädiktoren als Zahlen wie das Gewicht eines Spielers vorstellen, z. B. 135, 302 oder 274. Manchmal wird für kontinuierliche Prädiktorvariablen auch der Begriff Steigung verwendet. Down ist ein Beispiel für eine diskrete Prädiktorvariable. Informell betrachtet sind diskrete Prädiktoren Gruppen oder Kategorien wie die Position (z. B. Running Back oder Quarterback). Standardmäßig behandelt die Option formula in statsmodels und Base R diskrete Prädiktoren als Kontraste.

Schauen wir uns die Beispielformel an: rushing_yards ~ ydstogo + down. R würde einen Intercept für das niedrigste (oder alphabetisch erste) Down schätzen und dann den Kontrast oder die Differenz für die anderen Downs. So würden zum Beispiel vier Prädiktoren geschätzt: ein Intercept, also der Mittelwert rushing_yards für die Yards, die noch zu gehen sind, ein Kontrast für Second Downs im Vergleich zum First Down, ein Kontrast für Third Downs im Vergleich zum First Down und ein Kontrast für Four Downs im Vergleich zum First Down. Um dies zu sehen, schau dir die Designmatrix oder Modellmatrix in R an (Python hat ähnliche Funktionen, die hier nicht gezeigt werden). Erstelle zunächst einen Demonstrationsdatensatz:

## R
library(tidyverse)

demo_data_r <- tibble(down = c("first", "second"),
                      ydstogo = c(10, 5))

Anschließend erstellst du eine Modellmatrix, indem du die rechte Seite der Formel und die Funktion model.matrix() verwendest:

## R
model.matrix(~ ydstogo + down,
             data = demo_data_r)

Das Ergebnis ist:

  (Intercept) ydstogo downsecond
1           1      10          0
2           1       5          1
attr(,"assign")
[1] 0 1 2
attr(,"contrasts")
attr(,"contrasts")$down
[1] "contr.treatment"

Beachte, dass die Ausgabe drei Spalten hat: einen Achsenabschnitt, eine Steigung für ydstogo und einen Kontrast für die zweite Stufe (downsecond).

Du kannst aber auch einen Intercept für jedes Down schätzen, indem du ein -1 verwendest, z. B. rushing_yards ~ ydstogo + down -1. Damit würdest du vier Prädiktoren schätzen: einen Intercept für das erste Down, einen Intercept für das zweite Down, einen Intercept für das dritte Down und einen Intercept für das vierte Down. Verwende R, um dir die Beispielmodellmatrix anzusehen:

## R
model.matrix(~ ydstogo + down - 1,
             data = demo_data_r)

Das Ergebnis ist:

  ydstogo downfirst downsecond
1      10         1          0
2       5         0          1
attr(,"assign")
[1] 1 2 2
attr(,"contrasts")
attr(,"contrasts")$down
[1] "contr.treatment"

Beachte, dass du die gleiche Anzahl an Spalten hast wie vorher, aber jedes Down hat seine eigene Spalte.

Warnung

Computersprachen wie Python und R werden durch einige Gruppen wie "down" verwirrt. Der Computer versucht, diese Prädiktoren als kontinuierlich zu behandeln, z. B. die Zahlen 1, 2, 3 und 4 statt first down, second down, third down und fourth down. In pandas änderst du down in eine Zeichenkette (str), und in R änderst du down in ein Zeichen.

Die Formel für die multiple Regression lässt viele diskrete und kontinuierliche Prädiktoren zu. Wenn mehrere diskrete Prädiktoren vorhanden sind, wie z. B. down + team, kann die erste Variable (in diesem Falldown) entweder als Intercept oder als Kontrastparameter geschätzt werden. Alle anderen diskreten Prädiktoren werden als Kontraste geschätzt, wobei die ersten Gruppierungen als Teil des Intercepts behandelt werden. Anstatt über Steigungen und Abschnitte nachzudenken, kannst du die geschätzten Prädiktorvariablen für multiple Regression mit dem Begriff Koeffizienten beschreiben.

Explorative Datenanalyse

Im Fall der Rushing Yards von wirst du die folgenden Variablen als Merkmale im multiplen linearen Regressionsmodell verwenden: Down (down), Distanz (ydstogo), Yards bis zur Endzone (yardline_100), Laufort (run_location) und Punktedifferenz (score_differential). Es könnten natürlich auch andere Variablen verwendet werden, aber im Moment verwendest du diese Variablen vor allem deshalb, weil sie alle auf die eine oder andere Weise die Rushing Yards beeinflussen.

Zuerst lädst du die Daten und Pakete, die du verwenden willst, filterst nur die Laufdaten (wie in Kapitel 3) und entfernst Spielzüge, die keine regulären Downs waren. Mach das mit Python:

import pandas as pd
import numpy as np
import nfl_data_py as nfl
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
import seaborn as sns

seasons = range(2016, 2022 + 1)
pbp_py = nfl.import_pbp_data(seasons)

pbp_py_run = \
    pbp_py\
    .query('play_type == "run" & rusher_id.notnull() &' +
           "down.notnull() & run_location.notnull()")\
    .reset_index()

pbp_py_run\
    .loc[pbp_py_run.rushing_yards.isnull(), "rushing_yards"] = 0

Oder mit R:

library(tidyverse)
library(nflfastR)

pbp_r <- load_pbp(2016:2022)

pbp_r_run <-
    pbp_r |>
    filter(play_type == "run" & !is.na(rusher_id) &
        !is.na(down) & !is.na(run_location)) |>
    mutate(rushing_yards = ifelse(is.na(rushing_yards),
        0,
        rushing_yards
    ))

Als Nächstes erstellen wir mit Python ein Histogramm für Downs und Rushing Yards, wie in Abbildung 4-1 dargestellt:

## Python
# Change theme for chapter
sns.set_theme(style="whitegrid", palette="colorblind")

# Change down to be an integer
pbp_py_run.down =\
    pbp_py_run.down.astype(str)

# Plot rushing yards by down
g = \
    sns.FacetGrid(data=pbp_py_run,
                  col="down", col_wrap=2);
g.map_dataframe(sns.histplot, x="rushing_yards");
plt.show();
Abbildung 4-1. Histogramm der Rushing Yards nach Downs mit seaborn

Oder, Abbildung 4-2 mit R:

## R
# Change down to be an integer
pbp_r_run <-
    pbp_r_run |>
    mutate(down = as.character(down))

# Plot rushing yards by down
ggplot(pbp_r_run, aes(x = rushing_yards)) +
    geom_histogram(binwidth = 1) +
    facet_wrap(vars(down), ncol = 2,
               labeller = label_both) +
    theme_bw() +
    theme(strip.background = element_blank())
Abbildung 4-2. Histogramm der Rushing Yards nach Downs mit ggplot2

Das ist interessant, denn es sieht so aus, als ob Downs die Rushing Yards verringern. Die Daten haben jedoch einen Störfaktor, nämlich dass Rushes oft bei späten Downs mit geringeren Abständen stattfinden. Betrachten wir nur die Situationen, in denen ydstogo == 10. Erstelle in Python Abbildung 4-3:

## Python
sns.boxplot(data=pbp_py_run.query("ydstogo == 10"),
            x="down",
            y="rushing_yards");
plt.show()
Abbildung 4-3. Boxplot der Rushing Yards nach Downs für Spielzüge mit noch 10 Yards zu gehen (seaborn)

Oder erstelle mit R Abbildung 4-4:

## R
pbp_r_run |>
    filter(ydstogo == 10) |>
    ggplot(aes(x = down, y = rushing_yards)) +
    geom_boxplot() +
    theme_bw()
Abbildung 4-4. Boxplot der Rushing Yards nach Downs für Spielzüge mit noch 10 Yards zu gehen (ggplot2)

OK, jetzt siehst du, was du erwartest. Dies ist ein Beispiel für das Simpson-Paradoxon: Die Einbeziehung einer zusätzlichen, dritten Gruppierungsvariable verändert die Beziehung zwischen den beiden anderen Variablen. Nichtsdestotrotz ist klar, dass Down die Rushing Yards in einem Spielzug beeinflusst und daher berücksichtigt werden sollte. Ähnlich verhält es sich mit den Yards zur Endzone in seaborn in Abbildung 4-5 (und ändere die Transparenz mit scatter_kws={'alpha':0.25} und die Farbe der Regressionslinie mit line_kws={'color': 'red'}):

## Python
sns.regplot(
    data=pbp_py_run,
    x="yardline_100",
    y="rushing_yards",
    scatter_kws={"alpha": 0.25},
    line_kws={"color": "red"}
);
plt.show();
Abbildung 4-5. Streudiagramm mit linearer Trendlinie für die Ballposition (Yards bis zur Endzone) und Rushing Yards aus einem Spielzug (seaborn)

Oder erstelle mit R die Abbildung 4-6 (und ändere die Transparenz mit alpha = 0.25, damit du die überlappenden Punkte besser sehen kannst):

## R
ggplot(pbp_r_run, aes(x = yardline_100, y = rushing_yards)) +
    geom_point(alpha = 0.25) +
    stat_smooth(method = "lm") +
    theme_bw()
Abbildung 4-6. Streudiagramm mit linearer Trendlinie für die Ballposition (Yards bis zur Endzone) und Rushing Yards aus einem Spielzug (ggplot2)

Das scheint nicht viel zu bewirken, aber schauen wir uns mal an, was passiert, nachdem du mit Python den Durchschnitt gebildet hast:

## Python
pbp_py_run_y100 =\
    pbp_py_run\
    .groupby("yardline_100")\
    .agg({"rushing_yards": ["mean"]})

pbp_py_run_y100.columns =\
    list(map("_".join, pbp_py_run_y100.columns))

pbp_py_run_y100.reset_index(inplace=True)

Verwende diese Ergebnisse nun, um Abbildung 4-7 zu erstellen:

sns.regplot(
    data=pbp_py_run_y100,
    x="yardline_100",
    y="rushing_yards_mean",
    scatter_kws={"alpha": 0.25},
    line_kws={"color": "red"}
);
plt.show();
Abbildung 4-7. Streudiagramm mit linearer Trendlinie für Ballposition und Rushing Yards für nach Yards unterteilte Daten (seaborn)

Oder erstelle mit R Abbildung 4-8:

## R
pbp_r_run |>
    group_by(yardline_100) |>
    summarize(rushing_yards_mean = mean(rushing_yards)) |>
    ggplot(aes(x = yardline_100, y = rushing_yards_mean)) +
    geom_point() +
    stat_smooth(method = "lm") +
    theme_bw()
Abbildung 4-8. Streudiagramm mit linearer Trendlinie für Ballposition und Rushing Yards für nach Yards unterteilte Daten (ggplot2)

Die Abbildungen 4-7 und 4-8 zeigen einige Einblicke in den Fußball. Laufspielzüge, die weniger als 15 Yards lang sind, werden durch die Distanz begrenzt, weil die Entfernung zur Endzone begrenzt ist und die Verteidigung in der Redzone härter ist. Ebenso führen Spielzüge mit mehr als 90 Yards vor dem Ziel das Team aus seiner eigenen Endzone heraus. Die Verteidigung wird also alles daran setzen, einen Safety zu erzwingen, und die Offense wird eher einen Punt machen oder konservativ spielen, um einen Safety zu vermeiden.

Hier zeigt sich ein klarer positiver (aber nicht linearer) Zusammenhang zwischen den durchschnittlichen Rushing Yards und den Yards bis zur Endzone, sodass es von Vorteil ist, dieses Merkmal in die Modelle aufzunehmen. Unter "Annahme der Linearität" kannst du sehen, was mit dem Modell passiert, wenn Werte unter 15 Yards oder über 90 Yards entfernt werden. In der Praxis können kompliziertere Modelle mit diesen Nichtlinearitäten effektiv umgehen, aber das heben wir uns für ein anderes Buch auf. In Abbildung 4-9 siehst du dir den Laufort mit Python an:

## Python
sns.boxplot(data=pbp_py_run,
            x="run_location",
            y="rushing_yards");
plt.show();
Abbildung 4-9. Boxplot der Rushing Yards nach Laufort (seaborn)

Oder mit R in Abbildung 4-10:

## R
ggplot(pbp_r_run, aes(run_location, rushing_yards)) +
    geom_boxplot() +
    theme_bw()
Abbildung 4-10. Boxplot der Rushing Yards nach Laufort (ggplot2)

Nicht nur die Mittelwerte sind hier leicht unterschiedlich, sondern auch die Varianzen/Interquartilsbereiche scheinen zu variieren, also behalte sie in den Modellen bei. Eine weitere Bemerkung zu den Lauforten ist, dass das 75. Unabhängig davon, ob ein Spieler nach links, rechts oder durch die Mitte läuft, läuft er in drei Vierteln der Fälle 10 Yards oder weniger. Nur in den seltensten Fällen gibt es einen langen Rush.

Als Letztes schau dir die Punktedifferenz an und verwende dabei das Binning und die Aggregation, die du in Python für die Yards bis zur Endzone verwendet hast:

## Python
pbp_py_run_sd = \
    pbp_py_run\
    .groupby("score_differential")\
    .agg({"rushing_yards": ["mean"]}
)

pbp_py_run_sd.columns =\
     list(map("_".join, pbp_py_run_sd.columns))

pbp_py_run_sd.reset_index(inplace=True)

Verwende diese Ergebnisse nun, um Abbildung 4-11 zu erstellen:

## Python
sns.regplot(
    data=pbp_py_run_sd,
    x="score_differential",
    y="rushing_yards_mean",
    scatter_kws={"alpha": 0.25},
    line_kws={"color": "red"}
);
plt.show();
Abbildung 4-11. Streudiagramm mit linearer Trendlinie für die Punktedifferenz und Rushing Yards, für nach Punktedifferenz sortierte Daten (seaborn)

Oder erstelle in R Abbildung 4-12:

## R
pbp_r_run |>
    group_by(score_differential) |>
    summarize(rushing_yards_mean = mean(rushing_yards)) |>
    ggplot(aes(score_differential, rushing_yards_mean)) +
    geom_point() +
    stat_smooth(method = "lm") +
    theme_bw()
Abbildung 4-12. Streudiagramm mit linearer Trendlinie für die Punktedifferenz und Rushing Yards, für nach Punktedifferenz sortierte Daten (ggplot2)

Du kannst eine klare negative Beziehung erkennen, wie zuvor vermutet. Daher lässt du die Punktedifferenz im Modell.

Tipp

Wenn du in diesem Buch Code für Diagramme siehst, überlege, wie du die Diagramme verbessern könntest. Sieh dir den Code an, und wenn du einige Argumente nicht verstehst, suche und finde heraus, wie du den Plot-Code ändern kannst. Der beste Weg, ein besserer Datenplotter zu werden, ist, deine eigenen Plots zu erforschen und zu erstellen.

Anwendung der multiplen linearen Regression

wende nun die multiple lineare Regression an, um die Rushing Yards over Expected (RYOE) zu ermitteln. In diesem Abschnitt gehen wir kurz auf die Schritte ein, die in Kapitel 3 behandelt werden.

Passe zunächst das Modell an. Speichere dann die berechneten Residuen als RYOE. Erinnere dich daran, dass die Residuen die Differenz zwischen dem vom Modell vorhergesagten Wert und dem in den Daten beobachteten Wert sind. Man könnte sie direkt berechnen, indem man die beobachteten Rushing Yards nimmt und die vorhergesagten Rushing Yards vom Modell abzieht. Die Residuen werden jedoch häufig in der Statistik verwendet, und sowohl Python als auch R beziehen die Residuen in die Anpassung des Modells mit ein. Diese Herleitung unterscheidet sich von der Methode in Kapitel 3, weil du ein komplizierteres Modell erstellt hast.

Das Modell sagt rushing_yards voraus, indem es Folgendes erstellt:

  • Ein Schnittpunkt (1)

  • Ein Begriff, der das zweite, dritte und vierte Down dem ersten Down gegenüberstellt (down)

  • Ein Koeffizient für ydstogo

  • Eine Interaktion zwischen ydstogo und down, die einen ydstogo Kontrast für jeden down (ydstogo:down) schätzt.

  • Ein Koeffizient für Yards bis zur Endzone (yardline_100)

  • Die Position des laufenden Spiels auf dem Spielfeld (run_location)

  • Die Differenz zwischen den Punktzahlen der beiden Teams (score_differential)

Hinweis

Es gibt mehrere Ansätze, um Formeln zur Angabe von Wechselwirkungen zu verwenden. Der längste, aber einfachste Ansatz für unser Beispiel wäre down + ydstogo + as.factor(down):ydstogo. Sie kann als down * ydstogo abgekürzt werden. So könnte die Beispielformel rushing_yards ~ 1 + down + ydstogo + down:ydstogo + yardline_100 + run_location + score_differential,auch als rushing_yards ~ down * ydstogo + yardline_100 + run_location + score_differentialgeschrieben werden und spart drei Begriffe.

In Python kannst du das Modell mit dem Paket statsmodels anpassen und die Residuen als RYOE speichern:

## Python
pbp_py_run.down =\
    pbp_py_run.down.astype(str)

expected_yards_py =\
    smf.ols(
        data=pbp_py_run,
        formula="rushing_yards ~ 1 + down + ydstogo + " +
        "down:ydstogo + yardline_100 + " +
        "run_location + score_differential")\
        .fit()

pbp_py_run["ryoe"] =\
    expected_yards_py.resid
Hinweis

Wir fügen Zeilenumbrüche in unseren Code ein, damit er auf die Seiten dieses Buches passt. In Python verwenden wir zum Beispiel die Zeichenfolge "rushing_yards ~ 1 + down + ydstogo + ", gefolgt von + und einem Zeilenumbruch. Die nächsten beiden Zeichenketten, "down:ydstogo + yardline_100 + " und "run_location + score_differential", erhalten jeweils einen eigenen Zeilenumbruch, und wir verwenden das Zeichen +, um die Zeichenketten zusammenzufügen. Diese Zeilenumbrüche sind nicht erforderlich, helfen aber dabei, den Code für das menschliche Auge besser aussehen zu lassen und ihn im Idealfall besser lesbar zu machen.

Passe das Modell ebenfalls an und speichere die Residuen als RYOE in R:

## R
pbp_r_run <-
    pbp_r_run |>
    mutate(down = as.character(down))

expected_yards_r <-
    lm(rushing_yards ~ 1 + down + ydstogo + down:ydstogo +
       yardline_100 + run_location + score_differential,
       data = pbp_r_run
    )

pbp_r_run <-
    pbp_r_run |>
    mutate(ryoe = resid(expected_yards_r))

Untersuche nun die Zusammenfassung des Modells in Python:

## Python
print(expected_yards_py.summary())

Das Ergebnis ist:

                            OLS Regression Results
==============================================================================
Dep. Variable:          rushing_yards   R-squared:                       0.016
Model:                            OLS   Adj. R-squared:                  0.016
Method:                 Least Squares   F-statistic:                     136.6
Date:                Sun, 04 Jun 2023   Prob (F-statistic):          3.43e-313
Time:                        09:36:43   Log-Likelihood:            -2.9764e+05
No. Observations:               91442   AIC:                         5.953e+05
Df Residuals:                   91430   BIC:                         5.954e+05
Df Model:                          11
Covariance Type:            nonrobust
===============================================================================
                  coef         std err     t       P>|t|       [0.025   0.975]
------------------------------------------------------------------------------
Intercept          1.6085      0.136     11.849      0.000      1.342   1.875
down[T.2.0]        1.6153      0.153     10.577      0.000      1.316   1.915
down[T.3.0]        1.2846      0.161      7.990      0.000      0.969   1.600
down[T.4.0]        0.2844      0.249      1.142      0.254     -0.204   0.773
run_location[T.middle]
                  -0.5634      0.053    -10.718      0.000     -0.666  -0.460
run_location[T.right]
                  -0.0382      0.049     -0.784      0.433     -0.134   0.057
ydstogo            0.2024      0.014     14.439      0.000      0.175   0.230
down[T.2.0]:ydstogo
                  -0.1466      0.016     -8.957      0.000     -0.179  -0.115
down[T.3.0]:ydstogo
                  -0.0437      0.019     -2.323      0.020     -0.081  -0.007
down[T.4.0]:ydstogo
                   0.2302      0.090      2.567      0.010      0.054   0.406
yardline_100       0.0186      0.001     21.230      0.000      0.017   0.020
score_differential
                  -0.0040      0.002     -2.023      0.043     -0.008  -0.000
==============================================================================
Omnibus:                    80510.527   Durbin-Watson:                   1.979
Prob(Omnibus):                  0.000   Jarque-Bera (JB):          3941200.520
Skew:                           4.082   Prob(JB):                         0.00
Kurtosis:                      34.109   Cond. No.                         838.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors
is correctly specified.

Oder sieh dir die Zusammenfassung des Modells in R an:

## R
print(summary(expected_yards_r))

Das Ergebnis ist:

Call:
lm(formula = rushing_yards ~ 1 + down + ydstogo + down:ydstogo +
    yardline_100 + run_location + score_differential, data = pbp_r_run)

Residuals:
    Min      1Q  Median      3Q     Max
-32.233  -3.130  -1.173   1.410  94.112

Coefficients:
                    Estimate Std. Error t value Pr(>|t|)
(Intercept)         1.608471   0.135753  11.849  < 2e-16 ***
down2               1.615277   0.152721  10.577  < 2e-16 ***
down3               1.284560   0.160775   7.990 1.37e-15 ***
down4               0.284433   0.249106   1.142   0.2535
ydstogo             0.202377   0.014016  14.439  < 2e-16 ***
yardline_100        0.018576   0.000875  21.230  < 2e-16 ***
run_locationmiddle -0.563369   0.052565 -10.718  < 2e-16 ***
run_locationright  -0.038176   0.048684  -0.784   0.4329
score_differential -0.004028   0.001991  -2.023   0.0431 *
down2:ydstogo      -0.146602   0.016367  -8.957  < 2e-16 ***
down3:ydstogo      -0.043703   0.018814  -2.323   0.0202 *
down4:ydstogo       0.230179   0.089682   2.567   0.0103 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 6.272 on 91430 degrees of freedom
Multiple R-squared:  0.01617,   Adjusted R-squared:  0.01605
F-statistic: 136.6 on 11 and 91430 DF,  p-value: < 2.2e-16

Jeder geschätzte Koeffizient hilft dir, eine Geschichte über die Rushing Yards während der Spielzüge zu erzählen:

  • Laufspiele beim zweiten Down (down[T.2.0] in Python oder down2 in R) haben bei sonst gleichen Bedingungen mehr erwartete Yards pro Carry als beim ersten Down - in diesem Fall etwa 1,6 Yards.

  • Laufspiele beim dritten Down (down[T.3.0] in Python oder down3 in R) haben mehr erwartete Yards pro Carry als das erste Down, wenn alles andere gleich bleibt - in diesem Fall etwa 1,3 Yards.

  • Der Interaktionsterm sagt dir, dass dies vor allem dann der Fall ist, wenn weniger Yards für das First Down verbleiben (die Interaktionsterme sind alle negativ). Aus Sicht des Fußballs bedeutet das, dass zweite und dritte Downs und kurze Yards für ein First Down oder einen Touchdown für die Offense günstiger sind als ein First Down und 10 Yards bis zum Touchdown.

  • Umgekehrt werden bei Laufspielen beim vierten Down etwas mehr Yards erzielt als beim ersten Down (down[T.4.0] in Python oder down4 in R), aber nicht so viele wie beim zweiten oder dritten Down.

  • Wenn die Anzahl der zu erlaufenden Yards steigt (ydstogo), steigen auch die zu erlaufenden Yards, wobei jedes zu erlaufende Yard etwa ein Fünftel eines Yards wert ist, wenn alles andere gleich bleibt. Das liegt daran, dass die Schätzung von ydstogo positiv ist.

  • Je weiter der Ball von der Endzone entfernt ist, desto mehr Yards pro Spielzug werden durch Rushing-Plays erzielt (etwa 0,02 pro Yard bis zur Endzone; yardline_100). Selbst wenn das Team beispielsweise noch 100 Yards zu gehen hat, bedeutet ein Koeffizient von 0,02, dass in dem Spielzug nur 2 zusätzliche Yards erlaufen werden, was im Vergleich zu anderen Koeffizienten keine großen Auswirkungen hat.

  • Rushing-Spielzüge in der Mitte des Feldes bringen etwa ein halbes Yard weniger ein als Spielzüge auf der linken Seite, basierend auf der Kontrastschätzung zwischen der mittleren und linken Seite des Feldes (run_location[T.middle] in Python oder run_locationmiddle in R).

  • Der negative score_differential Koeffizient weicht statistisch von 0 ab. Wenn Teams also vorne liegen (eine positive Punktedifferenz haben), erzielen sie beim durchschnittlichen Laufspiel weniger Yards pro Spiel. Dieser Effekt ist jedoch so gering (0,004) und daher im Vergleich zu anderen Koeffizienten unwichtig, dass er ignoriert werden kann (ein Vorsprung von 50 Punkten würde die Anzahl der Yards pro Spielzug nur um 0,2 verringern).

Anhand der Koeffizienten kannst du erkennen, dass es schwieriger ist, in die Mitte des Feldes zu laufen als nach außen, wenn alle anderen Faktoren gleich sind. Du siehst auch, dass die Entfernung und die Yards bis zur First-Down-Marke und zur Endzone die Rushing Yards positiv beeinflussen: Je weiter ein Offensivspieler vom Ziel entfernt ist, desto mehr wird die Defense diesem Offensivspieler im Durchschnitt zugestehen.

Tipp

Das kableExtra Paket in R hilft dabei, gut formatierte Tabellen in R Markdown- und Quarto-Dokumenten sowie auf dem Bildschirm zu erstellen. Du musst das Paket installieren, wenn du es noch nicht getan hast.

Tabellen sind eine weitere Möglichkeit, Regressionskoeffizienten darzustellen. Mit dem Paket broom kannst du zum Beispiel aufgeräumte Tabellen in R erstellen, die dann mit dem Paket kableExtra formatiert werden können, wie in Tabelle 4-1. Mit diesem Code nimmst du die Modellanpassung expected_yards_r und leitest das Modell weiter, um die aufgeräumten Modellergebnisse (einschließlich der 95 %-KIs) mit tidy(conf.int = TRUE) zu extrahieren. Dann wandelst du die Tabelle in eine kable Tabelle um und zeigst zwei Ziffern mit kbl(format = "pipe", digits = 2) an. Zuletzt wendest du das Styling aus dem Paket kableExtra an, indem du kable_styling() verwendest:

## R
library(broom)
library(kableExtra)
expected_yards_r |>
    tidy(conf.int = TRUE) |>
    kbl(format = "pipe", digits = 2) |>
    kable_styling()
Tabelle 4-1. Beispiel für eine Tabelle mit Regressionskoeffizienten. term ist der Regressionskoeffizient, estimate ist der geschätzte Wert für den Koeffizienten, std.error ist der Standardfehler, statistic ist der t-Wert, p.value ist der p-Wert, conf.low ist der untere Teil des 95%-KI und conf.high ist der obere Teil des 95%-KI.
Begriff estimate std.error statistic p.value conf.low conf.high

(Intercept)

1.61

0.14

11.85

0.00

1.34

1.87

down2

1.62

0.15

10.58

0.00

1.32

1.91

down3

1.28

0.16

7.99

0.00

0.97

1.60

down4

0.28

0.25

1.14

0.25

-0.20

0.77

ydstogo

0.20

0.01

14.44

0.00

0.17

0.23

yardline_100

0.02

0.00

21.23

0.00

0.02

0.02

run_locationmiddle

-0.56

0.05

-10.72

0.00

-0.67

-0.46

run_locationright

-0.04

0.05

-0.78

0.43

-0.13

0.06

score_differential

0.00

0.00

-2.02

0.04

-0.01

0.00

down2:ydstogo

-0.15

0.02

-8.96

0.00

-0.18

-0.11

down3:ydstogo

-0.04

0.02

-2.32

0.02

-0.08

-0.01

down4:ydstogo

0.23

0.09

2.57

0.01

0.05

0.41

Tipp

Es kann schwierig sein, über Regressionen zu schreiben, und es ist wichtig, dein Publikum und seinen Hintergrund zu kennen. Der Absatz "Die Koeffizienten..." wäre zum Beispiel für einen Blog geeignet, aber nicht für eine Sportzeitschrift mit Peer-Review. Ebenso könnte eine Tabelle wie Tabelle 4-1in einem Zeitschriftenartikel enthalten sein, wird aber eher in einem technischen Bericht oder in den zusätzlichen Materialien eines Zeitschriftenartikels verwendet. Unsere Aufzählung der einzelnen Koeffizienten in Form einer Aufzählung könnte für einen Bericht an einen Kunden geeignet sein, der eine Beschreibung der einzelnen Elemente wünscht, oder für ein Lehrmittel wie einen Blog oder ein Buch über Fußball analytics.

Analysieren von RYOE

Genau wie mit der ersten Version von RYOE in Kapitel 3, wirst du jetzt die neue Version deiner Metrik für Rusher analysieren. Erstelle zunächst die Übersichtstabellen für die RYOE-Gesamtwerte, den Mittelwert und die Yards pro Carry in Python. Als nächstes speicherst du nur die Daten von Spielern mit mehr als 50 Carrys. Benenne die Spalten um und sortiere sie nach Gesamt-RYOE:

## Python
ryoe_py =\
    pbp_py_run\
    .groupby(["season", "rusher_id", "rusher"])\
    .agg({
        "ryoe": ["count", "sum", "mean"],
        "rushing_yards": ["mean"]})

ryoe_py.columns =\
    list(map("_".join, ryoe_py.columns))
ryoe_py.reset_index(inplace=True)

ryoe_py =\
    ryoe_py\
    .rename(columns={
        "ryoe_count": "n",
        "ryoe_sum": "ryoe_total",
        "ryoe_mean": "ryoe_per",
        "rushing_yards_mean": "yards_per_carry"
    })\
    .query("n > 50")

print(ryoe_py\
    .sort_values("ryoe_total", ascending=False)
    )

Das Ergebnis ist:

      season   rusher_id      rusher  ...  ryoe_total  ryoe_per  yards_per_carry
1870    2021  00-0036223    J.Taylor  ...  471.232840  1.419376         5.454819
1350    2020  00-0032764     D.Henry  ...  345.948778  0.875820         5.232911
1183    2019  00-0034796   L.Jackson  ...  328.524757  2.607339         6.880952
1069    2019  00-0032764     D.Henry  ...  311.641243  0.807361         5.145078
1383    2020  00-0033293     A.Jones  ...  301.778866  1.365515         5.565611
...      ...         ...         ...  ...         ...       ...              ...
627     2018  00-0027029     L.McCoy  ... -208.392834 -1.294365         3.192547
51      2016  00-0027155  R.Jennings  ... -228.084591 -1.226261         3.344086
629     2018  00-0027325    L.Blount  ... -235.865233 -1.531592         2.714286
991     2019  00-0030496      L.Bell  ... -338.432836 -1.381359         3.220408
246     2016  00-0032241    T.Gurley  ... -344.314622 -1.238542         3.183453

[533 rows x 7 columns]

Als Nächstes sortierst du in Python nach dem durchschnittlichen RYOE pro Übertrag:

## Python
print(
    ryoe_py\
    .sort_values("ryoe_per", ascending=False)
    )

Das Ergebnis ist:

      season   rusher_id        rusher ...  ryoe_total  ryoe_per  yards_per_carry
2103    2022  00-0034796     L.Jackson ...  280.752317  3.899338  7.930556
1183    2019  00-0034796     L.Jackson ...  328.524757  2.607339  6.880952
1210    2019  00-0035228      K.Murray ...  137.636412  2.596913  6.867925
2239    2022  00-0036945      J.Fields ...  177.409631  2.304021  6.506494
1467    2020  00-0034796     L.Jackson ...  258.059489  2.186945  6.415254
...      ...         ...           ... ...         ...       ...              ...
1901    2021  00-0036414       C.Akers ... -129.834294 -1.803254  2.430556
533     2017  00-0032940  D.Washington ... -105.377929 -1.848736  2.684211
1858    2021  00-0035860       T.Jones ... -100.987077 -1.870131  2.629630
60      2016  00-0027791      J.Starks ... -129.298259 -2.052353  2.301587
1184    2019  00-0034799     K.Ballage ... -191.983153 -2.594367  1.824324

[533 rows x 7 columns]

Die gleichen Tabellen können auch in R erstellt werden:

## R
ryoe_r <-
    pbp_r_run |>
    group_by(season, rusher_id, rusher) |>
    summarize(
        n = n(), ryoe_total = sum(ryoe), ryoe_per = mean(ryoe),
        yards_per_carry = mean(rushing_yards)
    ) |>
    filter(n > 50)

ryoe_r |>
    arrange(-ryoe_total) |>
    print()

Das Ergebnis ist:

# A tibble: 533 × 7
# Groups:   season, rusher_id [533]
   season rusher_id  rusher        n ryoe_total ryoe_per yards_per_carry
    <dbl> <chr>      <chr>     <int>      <dbl>    <dbl>           <dbl>
 1   2021 00-0036223 J.Taylor    332       471.    1.42             5.45
 2   2020 00-0032764 D.Henry     395       346.    0.876            5.23
 3   2019 00-0034796 L.Jackson   126       329.    2.61             6.88
 4   2019 00-0032764 D.Henry     386       312.    0.807            5.15
 5   2020 00-0033293 A.Jones     221       302.    1.37             5.57
 6   2022 00-0034796 L.Jackson    72       281.    3.90             7.93
 7   2019 00-0031687 R.Mostert   190       274.    1.44             5.83
 8   2016 00-0033045 E.Elliott   342       274.    0.800            5.14
 9   2020 00-0034796 L.Jackson   118       258.    2.19             6.42
10   2021 00-0034791 N.Chubb     228       248.    1.09             5.52
# ℹ 523 more rows

Sortiere dann in R nach dem durchschnittlichen RYOE pro Übertrag:

## R
ryoe_r |>
    filter(n > 50) |>
    arrange(-ryoe_per) |>
    print()

Das Ergebnis ist:

# A tibble: 533 × 7
# Groups:   season, rusher_id [533]
   season rusher_id  rusher        n ryoe_total ryoe_per yards_per_carry
    <dbl> <chr>      <chr>     <int>      <dbl>    <dbl>           <dbl>
 1   2022 00-0034796 L.Jackson    72      281.      3.90            7.93
 2   2019 00-0034796 L.Jackson   126      329.      2.61            6.88
 3   2019 00-0035228 K.Murray     53      138.      2.60            6.87
 4   2022 00-0036945 J.Fields     77      177.      2.30            6.51
 5   2020 00-0034796 L.Jackson   118      258.      2.19            6.42
 6   2017 00-0027939 C.Newton     92      191.      2.08            6.17
 7   2020 00-0035228 K.Murray     70      144.      2.06            6.06
 8   2021 00-0034750 R.Penny     119      242.      2.03            6.29
 9   2019 00-0034400 J.Wilkins    51       97.8     1.92            6.02
10   2022 00-0033357 T.Hill       95      171.      1.80            6.05
# ℹ 523 more rows

Die vorangegangenen Ergebnisse ähneln denen aus Kapitel 3, stammen aber von einem Modell, das zusätzliche Merkmale bei der Schätzung der RYOE korrigiert.

Wenn es um die Gesamtzahl der RYOE geht, steht Jonathan Taylors Saison 2021 immer noch an erster Stelle, und zwar mit über 100 Yards mehr als erwartet als der nächstbeste Spieler, während Derrick Henry wieder ein paar Mal auftaucht. Nick Chubb ist einer der besten Läufer der ganzen Liga, seit er 2018 in der zweiten Runde gedraftet wurde, während Raheem Mostert vom NFC-Meister San Francisco 49ers 2019 gar nicht gedraftet wurde, es aber trotzdem auf die Liste schafft. Ezekiel Elliott von den Cowboys und Le'Veon Bell von den Pittsburgh Steelers, New York Jets, Chiefs und Ravens tauchen an verschiedenen Stellen auf: Auf sie gehen wir später ein.

Bei den RYOE pro Carry (für Spieler mit 50 oder mehr Carrys in einer Saison) glänzt Rashaad Penny brillant 2021 erneut. Aber die meisten Spieler in dieser Liste sind Quarterbacks, wie der 2019er Liga-MVP Lamar Jackson, die 2011er und 2019er First-Overall-Picks Cam Newton und Kyler Murray sowie der Taysom Hill von den New Orleans Saints. Justin Fields, der 2022 eine der besten Rushing-Saisons in der Geschichte der Quarterback-Position hatte, taucht ebenfalls auf.

Was die Stabilität dieser Kennzahl im Vergleich zu den Yards pro Carry angeht, erinnerst du dich daran, dass du im vorigen Kapitel einen leichten Anstieg der Stabilität feststellen konntest, der aber nicht unbedingt ausreicht, um zu sagen, dass RYOE pro Carry definitiv eine bessere Kennzahl als Yards pro Carry für Rusher ist. Lass uns diese Analyse wiederholen:

## Python
#  keep only the columns needed
cols_keep =\
    ["season", "rusher_id", "rusher",
     "ryoe_per", "yards_per_carry"]

# create current dataframe
ryoe_now_py =\
    ryoe_py[cols_keep].copy()

# create last-year's dataframe
ryoe_last_py =\
    ryoe_py[cols_keep].copy()

# rename columns
ryoe_last_py\
    .rename(columns = {'ryoe_per': 'ryoe_per_last',
                       'yards_per_carry': 'yards_per_carry_last'},
                       inplace=True)

# add 1 to season
ryoe_last_py["season"] += 1

# merge together
ryoe_lag_py =\
    ryoe_now_py\
    .merge(ryoe_last_py,
           how='inner',
           on=['rusher_id', 'rusher',
               'season'])

Untersuche dann die Korrelation für die Yards pro Carry:

## Python
ryoe_lag_py[["yards_per_carry_last", "yards_per_carry"]]\
    .corr()

Das Ergebnis ist:

                      yards_per_carry_last  yards_per_carry
yards_per_carry_last              1.000000         0.347267
yards_per_carry                   0.347267         1.000000

Wiederhole den Vorgang mit RYOE:

## Python
ryoe_lag_py[["ryoe_per_last", "ryoe_per"]]\
    .corr()

Das Ergebnis ist:

               ryoe_per_last  ryoe_per
ryoe_per_last       1.000000  0.373582
ryoe_per            0.373582  1.000000

Diese Berechnungen können auch mit R durchgeführt werden:

## R
# create current dataframe
ryoe_now_r <-
    ryoe_r |>
    select(-n, -ryoe_total)

# create last-year's dataframe
# and add 1 to season
ryoe_last_r <-
    ryoe_r |>
    select(-n, -ryoe_total) |>
    mutate(season = season + 1) |>
    rename(ryoe_per_last = ryoe_per,
           yards_per_carry_last = yards_per_carry)

# merge together
ryoe_lag_r <-
    ryoe_now_r |>
    inner_join(ryoe_last_r,
               by = c("rusher_id", "rusher", "season")) |>
    ungroup()

Wähle dann die beiden Yards-pro-Carry-Spalten aus und untersuche die Korrelation:

## R
ryoe_lag_r |>
    select(yards_per_carry, yards_per_carry_last) |>
    cor(use = "complete.obs")

Das Ergebnis ist:

                     yards_per_carry yards_per_carry_last
yards_per_carry             1.000000             0.347267
yards_per_carry_last        0.347267             1.000000

Wiederhole den Vorgang mit den RYOE-Spalten:

## R
ryoe_lag_r |>
    select(ryoe_per, ryoe_per_last) |>
    cor(use = "complete.obs")

Das Ergebnis ist:

               ryoe_per ryoe_per_last
ryoe_per      1.0000000     0.3735821
ryoe_per_last 0.3735821     1.0000000

Das ist ein interessantes Ergebnis! Die Stabilität der RYOE von Jahr zu Jahr verbessert sich mit dem neuen Modell geringfügig, was bedeutet, dass wir, nachdem wir mehr und mehr Kontext aus der Situation herausgenommen haben, tatsächlich ein zusätzliches Signal für die Fähigkeiten der Running Backs über den Erwartungen erhalten. Das Problem ist, dass es sich dabei immer noch um eine minimale Verbesserung handelt, da der Unterschied der r-Werte weniger als 0,03 beträgt. In weiteren Arbeiten sollte das Problem mit besseren Daten (z. B. Tracking-Daten) und besseren Modellen (z. B. baumbasierten Modellen) weiter erforscht werden.

Tipp

Du hast hoffentlich bemerkt, dass der Code in diesem Kapitel und in Kapitel 3sich wiederholt und ähnlich ist. Wenn wir den Code regelmäßig wiederholen würden, würden wir eine eigene Reihe von Funktionen schreiben und sie in ein Paket packen, damit wir unseren Code leicht wiederverwenden können."Pakete" bietet einen Überblick über dieses Thema.

Sind Running Backs also wichtig?

Diese Frage scheint auf den ersten Blick albern. Natürlich sind die Running Backs, also die Spieler, die den Ball 50 bis 250 Mal im Jahr tragen, wichtig. Jim Brown, Franco Harris, Barry Sanders, Marshall Faulk, Adrian Peterson, Derrick Henry, Jim Taylor-die Geschichte der NFL kann nicht geschrieben werden, ohne die Leistungen der großen Läufer zu erwähnen.

Tipp

Ben Baldwin betreibt einenNerd-zu-Mensch-Übersetzer, der uns hilft, unsere Wortwahl in diesem Kapitel zu beschreiben. Er stellt fest: "Was die Nerds sagen: Running Backs spielen keine Rolle". Und dann: "Was die Nerds meinen: Die Ergebnisse von Laufspielen werden in erster Linie durch das Blocking und die Verteidiger in der Box bestimmt, nicht dadurch, wer den Ball trägt. Running Backs sind austauschbar, und es macht keinen Sinn, viele Ressourcen (im Draft oder bei der Free Agency) zu investieren." Und dann liefert er die Beweise dafür. Auf seiner Seite findest du diese Beweise und weitere nützliche Tipps.

Wir sollten jedoch ein paar Dinge beachten. Erstens ist das Passen des Footballs im Laufe der letzten Jahrzehnte immer einfacher geworden. In "The Most Important Job in Sports Is Easier Than Ever Before" (Der wichtigste Job im Sport ist so einfach wie nie zuvor) stellt Kevin Clark fest, dass "Schemaänderungen, Regeländerungen und Veränderungen bei den Athleten" dazu beitragen, dass Quarterbacks mehr Yards passen. Wir fügen Kevins Liste noch die Technologie hinzu (z.B. die Handschuhe, die die Spieler tragen), sowie die Einführung von College-Offensiven und die kreativen Wege, auf denen sie den Football passen.

Mit anderen Worten: Die Vorstellung, dass beim Passen nur drei Dinge passieren können (ein Abschluss, eine Unvollständigkeit oder eine Interception), von denen zwei schlecht sind, berücksichtigt nicht die steigende Rate, mit der das erste Ereignis (ein Abschluss) eintritt. Das Passen war lange Zeit effizienter als das Laufen (siehe "Übungen"), aber jetzt ist die Varianz beim Passen so stark gesunken, dass es in den meisten Fällen der geringen Varianz (und der geringen Effizienz) beim Laufen vorzuziehen ist.

Abgesehen von der Tatsache, dass das Laufen des Balls weniger effizient ist als das Passen des Balls, sei es gemessen an den Yards pro Spielzug oder an den erwarteten Zusatzpunkten (EPA), hat der Spieler, der den Ball läuft, offensichtlich weniger Einfluss auf das Ergebnis eines Laufspiels als bisher angenommen.

Andere Faktoren, die wir mit den Daten von nflfastR nicht berücksichtigen konnten, wirken sich ebenfalls auf die Produktion von Running Backs aus. Eric hat während seiner Zeit bei PFF gezeigt, dass das Spiel der Offensive Line einen großen Einfluss auf die Rushing-Plays hat, indem er die PFF-Spielernoten der Offensive Linemen für ein bestimmtes Spiel verwendet hat.

Eric hat in der Saison 2021 auch gezeigt, dass das Konzept des perfekt geblockten Laufs - ein Laufspiel, bei dem kein Blocker ein negatives Spiel macht (z. B. von einem Verteidiger geschlagen wird) - das Ergebnis eines Laufspiels um etwa einen halben erwarteten Punkt verändert. Zwar kann der Running Back durch die Aufstellung seiner Blocker und Ähnliches dazu beitragen, dass ein perfekt geblockter Spielzug zustande kommt, aber der Einfluss eines einzelnen Running Backs dürfte nicht annähernd so groß sein.

Die NFL hat dieses Phänomen im Allgemeinen aufgegriffen und in den letzten zehn Jahren immer weniger Geld für Running Backs ausgegeben, wie Benjamin Morris in "Running Backs Are Finally Getting Paid What They're Worth" von FiveThirtyEight zeigt. Wie dem auch sei, die Position ist nach wie vor umstritten, da einige Spieler, die als "Ausreißer" gelten, große Verträge erhalten, meist zum Leidwesen ihrer Teams.

Ein Beispiel dafür, dass ein Team seinen Star-Running Back überbezahlt hat und es wahrscheinlich bereut, war, als die Dallas Cowboys im Herbst 2019 Ezekiel Elliott für einen Sechsjahresvertrag über 90 Millionen Dollar mit 50 Millionen Dollar an Garantien verpflichteten. Elliott weigerte sich, einen neuen Vertrag abzuschließen, was den Cowboys-Besitzer Jerry Jones zweifellos an das Jahr 1993 erinnerte, als der beste Rusher der NFL, Emmitt Smith, die ersten beiden Spiele des Titelverteidigers Cowboys verpasste. Nachdem die Cowboys beide Spiele ohne Smith verloren hatten, gaben sie den Forderungen des Stars nach, und er belohnte sie mit der Auszeichnung zum MVP der NFL während der regulären Saison. Zum Abschluss der Saison wurde er auch zum Super Bowl MVP gewählt und führte Dallas zur zweiten von drei Meisterschaften Mitte der 90er Jahre.

Elliott hatte einen ähnlichen Karrierestart wie Smith: Er führte die NFL in jeder seiner ersten drei Saisons bis zu seinem Holdout bei den Rushing Yards pro Spiel an und führte die Liga bei den gesamten Rushing Yards in beiden Saisons an, in denen er das ganze Jahr spielte (Elliott war für Teile der Saison 2017 gesperrt). Die Cowboys haben ihre Division 2016 und 2018 gewonnen und 2018 erst zum zweiten Mal seit 1996 ein Playoff-Spiel gewonnen.

Wie von vielen Analytikern vorhergesagt, konnte Zeke den Deal nicht einhalten. Seine Rushing Yards pro Spiel fielen von 84,8 im Jahr 2019 auf 65,3 im Jahr 2020 und stagnierten bei 58,9 und 58,4 in den Jahren 2021 und 2022. Elliotts Yards pro Carry fielen 2022 von 4,0 auf 3,8 und er verlor schließlich in der Offseason 2023 seinen Startjob - und seinen Job insgesamt - an seinen Backup Tony Pollard.

Elliott hat nicht nur seinen Vertrag nicht erfüllt, sondern sein Vertrag war auch so belastend, dass die Cowboys produktive Spieler wie Wide Receiver Amari Cooper loswerden mussten, um unter die Gehaltsobergrenze zu kommen - ein Kaskadeneffekt, der das Team mehr geschwächt hat, als es das negative Spiel eines Running Backs könnte.

Ein Beispiel für ein Team, das sich behauptet und wahrscheinlich aufatmet, gab es 2018. Le'Veon Bell ( ), der in "Analyzing RYOE" auftauchte , blieb dem Trainingslager und der regulären Saison der Pittsburgh Steelers wegen eines Vertragsstreits fern. Er weigerte sich, mit dem Franchise Tagzu spielen - einemInstrument, mit dem ein Team einen Spieler für eine Saison zum Durchschnitt der fünf besten Gehälter auf seiner Position halten kann. Spieler, die in der Regel längerfristige Verträge wollen, sträuben sich oft dagegen, unter dieser Vertragsart zu spielen. Bell, der in seinen ersten vier Jahren bei den Pittsburgh Steelers mehr als 5.000 Yards auf dem Boden zurückgelegt hat und 2017 die NFL bei den Touches (Carries und Receptions) anführte, war ein solcher Spieler.

Das Problem für Bell war, dass er in Pittsburgh leicht zu ersetzen war. James Conner ( ), ein Krebsüberlebender von der University of Pittsburgh, der in der dritten Runde des NFL Draft 2017 ausgewählt wurde, erlief über 950 Yards bei 4,5 Yards pro Carry (Bells Karrieredurchschnitt lag bei 4,3 als Steeler) und erzielte 13 Touchdowns - und wurde in die Pro Bowl gewählt.

Im darauffolgenden Jahr durfte Bell zu den Jets wechseln, wo er 17 Spiele absolvierte, im Durchschnitt nur 3,3 Yards pro Carry erzielte und nur viermal traf. Nachdem er in der Saison 2020 entlassen wurde, landete er bei den Kansas City Chiefs. Dieses Team schaffte es zwar in den Super Bowl, aber er spielte dort nicht mit und die Chiefs verloren 31:9 gegen Tampa Bay. Nach dem Ende der Saison 2021 war er aus dem Football raus.

Die Geschichten von Elliott und Bell sind vielleicht die dramatischsten Fälle, in denen ein Starting Running Back in Ungnade gefallen ist, aber sie sind nicht die einzigen. Deshalb ist es wichtig, die Produktion sowohl dem Spieler als auch dem System und dem Rest des Kaders in einem angemessenen Verhältnis zuzuordnen, um keine Fehlinvestition in Bezug auf das Gehalt zu tätigen.

Annahme der Linearität

Die Abbildungen 4-7 und 4-8 zeigen eindeutig eine nichtlineare Beziehung. Technisch gesehen setzt die lineare Regression voraus, dass sowohl die Residuen normalverteilt sind als auch dass die beobachtete Beziehung linear ist. In der Regel gehen die beiden jedoch Hand in Hand.

Base R enthält nützliche Diagnosewerkzeuge, um die Ergebnisse von linearen Modellen zu untersuchen. Die Werkzeuge verwenden die Funktion plot() von Base R (dies ist eines der wenigen Male, in denen wir plot() mögen; es erstellt eine einfache Funktion, die wir nur für die interne Diagnose verwenden und nicht mit anderen teilen). Verwende zunächst par(mfrow=c(2,2)), um vier Subplots zu erstellen. Dann erstellst du mit plot() die multiple Regression, die du zuvor angepasst und als expected_yards_r gespeichert hast, um Abbildung 4-13 zu erstellen:

## R
par(mfrow = c(2, 2))
plot(expected_yards_r)

Abbildung 4-13 enthält vier Unterdiagramme. Oben links werden die geschätzten (oder angepassten) Werte des Modells mit der Differenz zwischen vorhergesagtem und angepasstem Wert (oder Restwert) verglichen. Oben rechts ist die kumulative Verteilung der Anpassungen des Modells im Vergleich zu den theoretischen Werten dargestellt. Unten links werden die angepassten Werte mit der Quadratwurzel der standardisierten Residuen verglichen. Rechts unten wird der Einfluss eines Parameters auf die Anpassung des Modells im Vergleich zu den standardisierten Residuen dargestellt.

Abbildung 4-13. Vier diagnostische Subplots für ein Regressionsmodell

Die Teilgrafik "Residuals vs. Fitted" sieht nicht allzu pathologisch aus. Sie zeigt lediglich, dass viele Datenpunkte nicht gut zum Modell passen und dass die Daten schief sind. Die Teilgrafik "Normal Q-Q" zeigt, dass viele Datenpunkte von dem erwarteten Modell abweichen. Das bedeutet, dass unser Modell in einigen Fällen nicht gut zu den Daten passt. Die Teilgrafik "Skalenlage" zeigt ähnliche Muster wie die Grafik "Residuals vs. Fitted". Auch hier gibt es ein merkwürdiges Muster mit W-ähnlichen Linien in der Nähe von 0, weil die Daten ganzzahlig sind (z. B. 0, 1, 2, 3). Schließlich zeigt "Residuals vs. Leverage", dass einige Datenbeobachtungen eine große "Hebelwirkung" auf die Schätzungen des Modells haben, aber diese liegen innerhalb des erwarteten Bereichs, basierend auf ihrem Cook's distance. Die Cook-Distanz ist informell der geschätzte Einfluss einer Beobachtung auf die Anpassung des Modells. Grundsätzlich bedeutet ein größerer Wert, dass eine Beobachtung einen größeren Einfluss auf die Schätzungen des Modells hat.

Schauen wir uns an, was passiert, wenn du Spielzüge unter 15 Yards oder über 90 Yards entfernst, um Abbildung 4-14 zu erstellen:

## R
expected_yards_filter_r <-
    pbp_r_run |>
    filter(rushing_yards > 15 & rushing_yards < 90) |>
    lm(formula = rushing_yards ~ 1 + down + ydstogo + down:ydstogo +
                 yardline_100 + run_location + score_differential)

par(mfrow = c(2, 2))
plot(expected_yards_filter_r)
Abbildung 4-14. Diese Abbildung ist eine Neuberechnung von Abbildung 4-13, wobei das Modell nur Rushing-Spielzüge mit mehr als 15 Yards und weniger als 95 Yards berücksichtigt.

Abbildung 4-14 zeigt, dass das neue lineare Modell, expected_yards_filter_r, besser passt. Obwohl der Subplot "Residuals vs. Fitted" eine schiefe Gerade aufweist (was darauf zurückzuführen ist, dass die Daten jetzt zensiert wurden), sehen die anderen Subplots besser aus. Der am meisten verbesserte Subplot ist "Normal Q-Q". Abbildung 4-13 hat eine Skala von -5 bis 15, während diese Darstellung jetzt eine Skala von -1 bis 5 hat.

Als letzte Überprüfung kannst du dir die Zusammenfassung des Modells ansehen und die verbesserte Modellanpassung feststellen. Die R 2 Wert verbesserte sich von ~0,01 auf 0,05:

## R
summary(expected_yards_filter_r)

Das Ergebnis ist:

Call:
lm(formula = rushing_yards ~ 1 + down + ydstogo + down:ydstogo +
    yardline_100 + run_location + score_differential, data = filter(pbp_r_run,
    rushing_yards > 15 & rushing_yards < 95))


Residuals:
    Min      1Q  Median      3Q     Max
-17.158  -7.795  -3.766   3.111  63.471


Coefficients:
                    Estimate Std. Error t value Pr(>|t|)
(Intercept)        21.950963   2.157834  10.173   <2e-16 ***
down2              -2.853904   2.214676  -1.289   0.1976
down3              -0.696781   2.248905  -0.310   0.7567
down4               0.418564   3.195993   0.131   0.8958
ydstogo            -0.420525   0.204504  -2.056   0.0398 *
yardline_100        0.130255   0.009975  13.058   <2e-16 ***
run_locationmiddle  0.680770   0.562407   1.210   0.2262
run_locationright   0.635015   0.443208   1.433   0.1520
score_differential  0.048017   0.019098   2.514   0.0120 *
down2:ydstogo       0.207071   0.224956   0.920   0.3574
down3:ydstogo       0.165576   0.234271   0.707   0.4798
down4:ydstogo       0.860361   0.602634   1.428   0.1535
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 12.32 on 3781 degrees of freedom
Multiple R-squared:  0.05074,   Adjusted R-squared:  0.04798
F-statistic: 18.37 on 11 and 3781 DF,  p-value: < 2.2e-16

Zusammenfassend lässt sich sagen, dass du anhand der Residuen des Modells erkennen kannst, dass das Modell bei Spielzügen, die kürzer als 15 Yards oder länger als 95 Yards sind, nicht gut funktioniert. Wenn du diese Einschränkung kennst und quantifizierst, weißt du zumindest, was du mit deinem Modell nicht weißt.

In diesem Kapitel verwendete Data Science Tools

In diesem Kapitel wurden die folgenden Themen behandelt:

  • Anpassen einer multiplen Regression in Python mit OLS() oder in R mit lm()

  • Verstehen und Lesen der Koeffizienten einer multiplen Regression

  • Wiederholte Anwendung der in früheren Kapiteln erlernten Tools zur Datenauswertung

  • Prüfung der Anpassung einer Regression

Übungen

  1. Ändere den Schwellenwert für Übertragungen von 50 auf 100 Übertragungen. Siehst du immer noch die Stabilitätsunterschiede, die du in diesem Kapitel gefunden hast?

  2. Verwende den vollständigen Datensatz von nflfastR, um zu zeigen, dass Rushing weniger effizient ist als Passing, sowohl in Bezug auf Yards pro Spiel als auch auf EPA pro Spiel. Untersuche auch die Variabilität dieser beiden Spieltypen.

  3. Ist Rushing in manchen Situationen (z.B. in der Nähe der gegnerischen Endzone) wertvoller als Passen?

  4. Schau dir die RYOE-Werte von James Conner im Vergleich zu denen von Bell in seiner Karriere an. Was fällt dir an den Werten der beiden Backs auf?

  5. Wiederhole die Prozesse in diesem Kapitel mit den Empfängern im Passspiel. Dazu musst du nach play_type == "pass" und receiver_id filtern, die nicht NA oder NULL sind. Es wird schwierig sein, Merkmale zu finden, aber orientiere dich an der Vorgehensweise in diesem Kapitel. Verwende zum Beispiel down und distance, aber verwende vielleicht auch etwas wie air_yards in deinem Modell, um eine Erwartungshaltung zu erzeugen.

Empfohlene Lektüre

Die unter "Empfohlene Lektüre" aufgeführten Bücher gelten auch für dieses Kapitel. Auf dieser Liste aufbauend, findest du hier einige andere Ressourcen, die du vielleicht hilfreich findest:

  • Der Chicago Guide to Writing about Numbers, 2. Auflage, von Jane E. Miller (Chicago Press, 2015) bietet großartige Beispiele für die Beschreibung von Zahlen in verschiedenen Formen des Schreibens.

  • The Chicago Guide to Writing about Multivariate Analysis, 2nd edition, von Jane E. Miller (University of Chicago Press, 2013) enthält viele Beispiele zur Beschreibung der multiplen Regression. Obwohl wir nicht damit einverstanden sind, dass sie multivariate Regression als Synonym für multiple Regression verwendet, liefert das Buch gute Beispiele für die Beschreibung von Regressionsergebnissen.

  • Practical Linear Algebra for Data Science von Mike X Cohen (O'Reilly Media, 2022) vermittelt ein Verständnis der linearen Algebra, das dir helfen wird, die Regression besser zu verstehen. Lineare Algebra bildet die Grundlage für fast alle statistischen Methoden, einschließlich der multiplen Regression.

  • FiveThirtyEight enthält viel Datenjournalismus und wurde von Nate Silver gegründet und betrieben, bis er sich 2023 von der ABC/Disney-eigenen Seite zurückzog. Sieh dir die Beiträge an und versuche zu erkennen, wo die Seite Regressionsmodelle für Beiträge verwendet.

  • Statistical Modeling, Causal Inference, and Social Science (Statistische Modellierung, Kausalschluss und Sozialwissenschaft) ist ein Blog von Andrew Gelman mit Beiträgen von vielen anderen Autoren, in dem häufig über Regressionsmodelle diskutiert wird. Gelman ist die akademischere Version von Nate Silver als Politikwissenschaftler.

  • Statistical Thinking von Frank Harrell ist ein Blog, in dem auch häufig die Regressionsanalyse diskutiert wird. Harrell ist eine eher akademische Version von Nate Silver, der sich normalerweise mehr auf Statistiken konzentriert. Viele seiner Beiträge sind jedoch auch für alle, die Regressionsanalysen durchführen, relevant.

Get Fußballanalyse mit Python & R now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.