Tutorial III. Anforderungen an Fälle stellen: Rangfolgen

Im dritten Teil des Tutorials zeige ich, wie man Richtwerte festlegen kann, um vergleichbare Stichproben zu generieren. Am Ende plotten wir ein erstes Diagramm. Es dreht sich um Colin Kaepernick.

9
287
5
(1)
Lesezeit: 7 Minuten

Wiederholung

Zunächst hier der Code, den ihr ablaufen lassen müsst, um am Anfangspunkt dieses Tutorials einsteigen zu können. Hier befassen wir uns weiter damit, wie man bestimmte Fälle auswählt. Dafür markiert ihr alle Zeilen und klickt auf Run oder drückt strg+Enter. Voraussetzung ist, dass ihr die packages bereits installiert habt. Wenn ihr wissen wollt, wie das geht und wie sich dieser Code zusammen setzt, könnt ihr in Teil I und II mehr erfahren.

library(devtools)
library(dplyr)
library(dbplyr)
library(nflfastR)
library(DBI) 
library(RSQLite)
update_db()
connection <- dbConnect(SQLite(), "./pbp_db")
connection
pbp_db <- tbl(connection, "nflfastR_pbp")

Kaep_pass <- pbp_db %>% filter(passer == "C.Kaepernick")
Kaep_pass <- Kaep_pass %>% group_by(season)
Kaep_pass_year <- Kaep_pass %>% summarise(wp, passer, epa = mean(epa, na.rm = TRUE), cpoe = mean(cpoe, na.rm = TRUE), wpa = mean(wpa, na.rm = TRUE), success = mean(success, na.rm = TRUE), air_epa = mean(air_epa, na.rm = TRUE), tot_yds = sum(yards_gained, na.rm = TRUE), plays = n()) %>% collect()

Wichtig: nehmt diesen Code als Grundlage, ich musste etwas vom letzten Mal ändern. Dazu später mehr.

Was wir bis jetzt gefiltert haben, sind die Passing-Snaps von Colin Kaepernick aus seinen aktiven Jahren. Beim letzten Mal haben wir gesehen, dass er 2011 nur fünf Snaps als Werfer hatte. Schauen wir uns die Fälle einzeln an: In welchem Viertel war der Snap, wieviel Zeit blieb bis zum Ende des Viertels und wie hoch war die Wahrscheinlichkeit, dass Kaepernicks Team gewinnt?

Kaep_2011 <- Kaep_pass %>% filter(season == "2011") %>% select(qtr, quarter_seconds_remaining, wp)
Kaep_2011
Colin Kaepernicks Passing Snaps 2011.

Wir sehen, dass in der Saison 2011 alle Snaps von Kaepernick im letzten Viertel gespielt wurden und die 49ers dabei mit an Sicherheit grenzender Wahrscheinlichkeit bereits als Sieger fest standen.

Einen Nachtrag muss ich noch machen: hier findet ihr eine Tabelle, in der alle Variablen der Datenbank von nflfastR inklusive Beschreibung aufgeführt sind. Wenn ihr euch also fragt, was diese und jene Abkürzung bedeutet und wie sie definiert ist, findet ihr dort eine Antwort.

Garbage Time

In vielen Spielen gibt es den Moment, wo alles entschieden ist. Paradebeispiel ist der Kneel-down des Quarterbacks kurz vor Ende. Damit macht ein Quarterback negative Yards, was sich auf fast alle Stats eher negativ auswirkt. Betrachtet man es anders herum, ändert ein Touchdown kurz vor Schluss bei aussichtslosem Rückstand wenig am Ausgang des Spiels, aber viel an den Quarterback Stats. Dieses Problems kann man sich entledigen, wenn man, wie wir heute, Fälle ausschließt. Wir wollen über trait statt über state reden, daher wollen wir vermeiden, extreme Situationen mit in die Analyse einzubeziehen.

Win Probabilty als zentraler Faktor

Vor jedem Snap gibt nflfastR die Siegwahrscheinlichkeit (wp) des Teams an, das den Ball hat. Dies wird für jeden Spielzug berechnet und ist Teil eures Datensatz’. Der Mittelwert von wp ist 0,5, die Standardabweichung ist 0,3. Das heißt, dass ein durchschnittlicher Snap mit einer Siegeswahrscheinlichkeit zwischen 0,2 und 0,8 beginnt. Liegt der Wert außerhalb dieses Bereichs, startet ein Spielzug in einer nicht durchschnittlichen Situation. Will man also die vermeintliche Leistung halbwegs unabhängig vom Ergebnis eruieren, bietet es sich an, nur jene Spielzüge heranzuziehen, die bei gewöhnlicher Aussicht auf den Sieg beginnen. Ihr werdet oft sehen, dass in Diagrammen 0.2 < wp < 0.8 eine der Rahmenbedingungen ist. Und so werden wir es auch hier im Folgenden halten. Daher die oben angesprochene Veränderung im Code: wp ist oben eine Variable von Kaep_pass_year, was sie im Tutorial II am Ende nicht war.

Angewendet auf Kaepernicks Pässe würde der Filter also so aussehen:

Kaep_pass_year <- Kaep_pass_year %>% filter(wp < 0.8 & wp > 0.2)

Fälle eingrenzen – was ist ein Backup?

Worauf wollten wir eigentlich hinaus? Ich wollte die Spielzeiten, in denen Colin Kaepernick gespielt hat, mit den Leistungen der Backups aus den Jahren danach vergleichen, als ihm kein Team einen Vertrag mehr gab. Dabei soll es hier nur um die passing stats gehen. Wie lege ich nun die Quarterbacks im Datensatz, die keine Starter waren, als Backups fest?

An diesem Punkt kommt es auf eine Mischung aus Genauigkeit und Kreativität an. Der Backup sollte in einer bestimmten Saison nicht die meisten Snaps als QB gespielt haben – das legen wir als Hauptkriterium fest. Um ihn jedoch irgendwie bewerten zu können, sollte er eine bestimmte Mindestanzahl an plays gecallt haben. Würde jeder Trickspielzug, in dem der Punter einen 80 Yard Touchdown wirft, Vergleichsgröße zu einer ganzen Saison von Kaepernick werden, würde dies das Bild verzerren. Aber der Reihe nach.

Zuerst wählen wir alle Saisons nach Kaepernick aus:

Post_Kaep <- pbp_db %>% filter(season > "2016")

Dann fassen wir die Passstatistiken jedes Werfers dieser Jahre ähnlich derer von Kaep zusammen, und schließen die Garbage Time aus:

Post_Kaep <- Post_Kaep %>% filter(play_type == "pass" & wp < 0.8 & wp > 0.2) %>% 
  select(epa, cpoe, wpa, success, air_epa, yards_gained, passer, season, passer, posteam) %>% 
  group_by(passer, season) %>% 
  summarise(posteam, epa = mean(epa, na.rm = TRUE), cpoe = mean(cpoe, na.rm = TRUE), wpa = mean(wpa, na.rm = TRUE), success = mean(success, na.rm = TRUE), air_epa = mean(air_epa, na.rm = TRUE), tot_yds = sum(yards_gained), plays = n())

Jetzt rufen wir die Daten dieser Fälle mittels

Post_Kaep <- Post_Kaep %>% collect()

aus der Datenbank ab. 255 Fälle gab es, in denen seit 2017 in einer Saison ein Spieler mindestens einen Pass geworfen. Unten seht ihr 10 Werfer, die über eine Saison zusammengefasst wurden, sowie deren Passstatistiken, sortiert nach deren Namen.

Zusammengefasste Passingstats pro Spieler und Saison sortiert nach Namen

Rangfolge und Perzentlile

Jetzt führen wir hier eine neue Variable ein, nennen wir sie Backup. Deren Hauptkriterium soll werden, dass pro Team und Saison der Spieler, der die meisten Pässe geworfen hat, ausgeschlossen wird. Dafür müssen wir die Pässe pro Team und Saison zunächst in eine Rangfolge bringen:

Backups <- Post_Kaep %>% group_by(posteam, season) %>%
  mutate(Qb_Rang = rank(-plays)) 

Der Befehl mutate() manipuliert Werte um. In diesem konkreten Fall lege ich pro Team und Saison (seht euch group_by() an) fest, dass eine Variable namens „Qb_Rang“ eingeführt wird, die die Anzahl der Spielzüge (plays) in der Saison pro Team in Rangfolge bringt und diesen einen absoluten Wert zuweist, einen Rang.

Zum Verständnis, schauen wir uns das für ein Team in einer Saison an:

Beispiel <- Backups %>% filter(posteam == "GB", season=="2017")
Beispiel %>% collect()
In diesem ausgewählten Fall wäre Aaron Rodgers Backup. Er liegt in der Rangfolge hinter Brett Hundley was plays im Jahr 2017 angeht
Rangfolge der Quarterbacks in Green Bay 2017 nach Anzahl der Passspielzüge

Wir sehen, dass in der Saison 2017 Brett Hundley mehr Pässe geworfen hat (Qb_Rang = 1), als Aaron Rodgers vor bzw. nach seiner Verletzung in jenem Jahr. Rodgers zählt also nach unserer Definition als Backup für diese Saison.

Jetzt schließen wir alle Werfer aus, die die Nummer eins im Team in einer Saison waren:

Backups <- Backups %>% filter(Qb_Rang != 1) %>% collect()

159 Fälle bleiben übrig. Dabei sind massenweise Trickspielzüge und Ähnliches enthalten. Wie filtern wir also die wirklichen Backup QBs heraus? Ich habe mich dafür entschieden, zwei Kennzahlen unserer verbliebenen Daten in Perzentile zu zerlegen.

Knapp geschildert heißt das, dass ich die 159 Reihen, also Daten pro Spieler und Saison anhand von zwei Variablen jeweils in eine Rangfolge bringe. Ich kann dabei mit einem Befehl eine neue Variable einführen, die beschreibt, in welchem Bereich ein Werfer auf einer bestimmte Variable liegt, verglichen mit dem Rest der Stichprobe. Möchten wir also abbilden, in welchem Perzentil die Anzahl der plays eines Quarterbacks in einer Saison verglichen mit allen Werfern zwischen 2017 bis 2019 liegt, dann geben wir zunächst folgenden Befehl ein:

Backups$perc_play <- ntile(Backups$plays, 100)

So legt ihr im Datensatz Backups eine neue Variable namens perc_play an, die pro Werfer und Saison den Perzentilrang beschreibt, den er in der Stichprobe bezüglich plays einnimmt.

Wir wollen uns aber nicht die schlechtesten Backups anschauen, daher nehmen wir noch als zweites Kriterium epa hinzu. Und da setzen wir unten an: wir schließen die schlechtesten 32% der EPA Leistungen von Werfern ab 2017 pro Saison aus. Wieso genau, darauf kommen wir noch. Oder ihr befasst euch selbst mit der Normalverteilung. Dazu wollen wir insgesamt nur die oberen 50% was plays angeht, anschauen:

Backups$perc_epa <- ntile(Backups$epa, 100)
Backups_final <- Backups %>% filter(perc_play > 50 & perc_epa > 32)

Das große Ganze: Fälle verbinden

Um jene übrigen Backups mit Kaepernick zu vergleichen, verbinden wir die Datensätze:

BLM <- rbind(Backups_final,Kaep_pass_year)

In Tabellenform sieht das dann, absteigend sortiert nach epa mittels arrange() so aus:

BLM %>% arrange(-epa)
Kaepernicks Werte aus seinen ersten Jahren sind Top 10 Werte im Vergleich zu den Backups nach ihm.
Passstatistiken ausgewählter Backup Quarterbacks 2017-2019 im Vergleich zu Colin Kaepernick 2012-2016

Exkurs und Ausblick: Das erste Diagramm

Wir sind weit gekommen und ich mag euch nicht aus diesem Text entlassen, ohne dass ihr ein buntes Bild generiert. Kopiert folgenden Code zunächst ungefragt, im nächsten Tutorial gibt es dazu mehr Erklärungen:

install.packages("ggimage")
install.packages("ggrepel")
library(ggimage)
library(ggrepel)
BLM %>%
  ggplot(aes(x = cpoe, y = epa, label = paste(passer, season))) +
  geom_hline(yintercept = mean(BLM$epa), color = "red", linetype = "dashed") +
  geom_vline(xintercept = mean(BLM$cpoe), color = "red", linetype = "dashed") +
  geom_point(color = if_else(BLM$passer == "C.Kaepernick", "red", "black"), cex=BLM$plays/20, alpha=1/3) +
  theme_bw() +
  geom_text_repel(point.padding = NA) +
  theme_bw() +
  labs(x = "Completion % above expectation (CPOE)",
       y = "EPA pro pass",
       title = "Passstatistiken von Colin Kaepernick 2012-2016 im Vergleich mit High-End Backups ab 2017",
       caption = "Daten: @nflfastR, Autor: @simon19481")
Der erste Plot. Was er zeigt, wie der Code aufgebaut ist, dazu kommen wir noch.

Beim nächsten Mal erkläre ich euch, wie sich der Code für das Diagramm aufbaut. Darüber hinaus werde ich wohl um einige Grundlagen der Statistik nicht herum kommen. Lest euch, wie gesagt, gerne in das Thema Normalverteilung ein. Und befasst euch mit Mittelwert, Median, Standardabweichung und Varianz. Und überdenkt, inwiefern ich nicht bereits einen selection bias fabriziert habe und welche weiteren Probleme und Einschränkungen meine gezeigte Herangehensweise hat.

Oder wartet die nächste Ausgabe ab.

Hat dir der Artikel gefallen?

Klicke auf die Sterne, um zu bewerten!

Durchschnittliche Bewertung: 5/5 (1 Stimmen)

Bisher keine Bewertungen! Sei der Erste, der diesen Beitrag bewertet.

Weil du diesen Beitrag nützlich fandest...

Folge uns in sozialen Netzwerken!

Es tut uns leid, dass der Beitrag für dich nicht hilfreich war!

Lasse uns diesen Beitrag verbessern!

Wie können wir diesen Beitrag verbessern?

9 KOMMENTARE

  1. Es funktioniert leider nicht und es werden auch garkeine Levels definiert, jedenfalls, wenn ich versuche, diese über levels() abzurufen. Liegt das evtl. am Pasten? (Leveln nach run_location funktioniert leider auch nicht)

  2. Ich konnte es noch nicht testen, aber funktioniert das bei dir? Bei mir werden, wenn ich die levels abfrage mit NULL keine angegeben. Mit der fehlenden Differenzierung bei den A-Hals muss man halt Leben.

  3. Ich hätte mal eine Frage. Ich versuche mir gerade, ähnlich wie NextGenStats eine Verteilung der erreichten Yards pro Rungap erstellen zu lassen. Dazu muss ich die vorgegebenen Gaps (guard, tackle, end) mit den Laufrichtungen (left, right) kombinieren. Ich möchte also unter der Variable run_gap_direction eine neuen Wert A_gap_left haben, wenn run_gap=”guard” und run_location=”left” ist. Welchen Operator benutze ich hierzu?

    • Mein Vorschlag wäre, dass du auf run_gap und run_location gruppierst (group_by()) und anschließend über summarise() deine Analyse durchführst. Dann hast du zwar erstmal eine Spalte mit run_gap und eine mit run_location, aber die kannst du ja zu einer zusammenfügen.

      Beispiel:
      data <- pbp %>%
      group_by(run_location, run_gap) %>%
      summarise(count = n(),
      ypc = mean(yards_gained, na.rm = T),
      epa_run = mean(epa, na.rm = T))

      • Großen Dank,

        das klappt soweit und ich habe unter der Kategorie gap run_gap und run_location zusammengefasst. Zwei Probleme habe ich noch:

        1. Ich würde gerne die Gaps aus Quarterbacksicht anordnen, also die Zeilen per Hand nach Namen umordnen und habe bisher nur austomatische Sortierfunktionen gefunden.

        2. Gib es in gplot2 auch Säulendiagramme, denen ich auch einen y-Wert zuordnen kann? Bei geom_bar kann ich ja nur die Anzahl einer Variable darstellen.

        • zur ersten frage kann ich dir nichts sagen. für die zweite bitte ich dich um etwas geduld: im nächsten teil der serie werde ich auf diagramm typen in ggplot eingehen. dauert nicht mehr lang 😉

          • Danke für den Tip! Ich habe mich ein bisschen ins Leveln reingelesen und das ist genau das, was ich theoretisch bräuchte, ich habe aber das Problem, dass es nicht funktioniert. Ich habe mal unten meinen Code hingeschrieben. Siehst du das Problem? (Es funktioniert auch nicht, run_location in “left” und “right” zu leveln, falls es ein Problem mit dem paste() wäre)

            RunningBacksGB % filter(posteam == “GB”)
            AJ % filter (rusher_player_name == “A.Jones”)
            AJ20 % filter (season == “2020”) %>% group_by (run_location, run_gap) %>%
            summarise(count = n(),ypc = mean(yards_gained, na.rm = T), epa_run = mean(epa, na.rm = T)) %>% mutate (gap = paste (run_location, run_gap)) %>% select (gap, run_location, run_gap, count, ypc, epa_run)

            gap_levels <- c("left end", "left tackle", "left guard", "right guard", "right tackle", "right end")
            AJ20$gap <- factor(AJ20$gap, levels = gap_levels)
            levels(run_location)
            print(AJ20)

            AJ20 <- A_Jones_2020
            AJ20$gap <- factor(AJ20$gap, levels = c("left end", "left tackle", "left guard", "right guard", "right tackle", "right end")

            ggplot(AJ20, aes(gap, ypc)) + geom_bar(stat ="identity")

          • Du hast hier die letzte Zeile vergessen, die den df neu sortiert.

            gap_levels <- c("left end", "left tackle", "left guard", "right guard", "right tackle", "right end") AJ20$gap <- factor(AJ20$gap, levels = gap_levels) AJ20<- test[order(AJ20$gap),]

            Mir ist aber noch ein inhaltliches Problem aufgefallen: Es gibt auch noch Runs up the middle. Ich nehme mal an, dass dies Runs durch eines der A Gaps sind. Left Guard wäre dann dementsprechend durch das linke B Gap, etc. Leider kann man nicht zwischen dem linken und dem rechten A Gap unterscheiden weil run_location = NA bei diesen Plays.

Schreibe eine Antwort

Scheibe deinen Kommentar
Sag uns deinen Namen