Analiza kontuzji graczy NFL – zrozumienie i wizualizacja danych

INTRO

Od jakiegoś czasu hobbystycznie (po godzinach) interesuję się analizą danych i szeroko pojętym uczeniem maszynowym (z ang. Machine Learning lub ML). Tu i tam często słyszymy o rozwoju nowych technologii i o tym w jak wielu dziedzinach algorytmy sztucznej inteligencji (AI) i uczenia maszynowego wpływają na nasze życie. Z wielu zastosowań AI/ML ogromna ilość ludzi nawet nie zdaje sobie sprawy. Mnie interesuje zastosowanie AI/ML w sporcie, czyli Computer Science in Sport albo krócej Sport Science. Poniżej pierwszy wpis pokazujący moją skromną analizę danych udostępnionych przez amerykańską ligę futbolu NFL na platformie Kaggle.

Szczegóły wyzwania i udostępnionych danych podane są tutaj.

SKRÓTOWY OPIS ZAGADNIENIA

NFL chce zbadać wpływ rodzaju nawierzchni, na której rozgrywane są mecze (naturalna lub syntetyczna) na ilość kontuzji kończyn dolnych (aby docelowo chronić zdrowie zawodników) – chodzi tutaj tylko o kontuzje wynikające z poruszania się zawodników na danym podłożu, a nie powstające wskutek zderzenia/faulów od innych zawodników.

NFL udostępnia 3 zbiory danych w postaci plików CSV (link do danych): zbiór opisujący kontuzje odniesione przez 100 graczy w ciągu dwóch regularnych sezonów, zbiór opisujący dokładną lokalizację, prędkość i kierunek ruchu 250 graczy (dane zbierane dzięki kamerom nagrywającym każdy mecz) oraz zbiór z danymi środowiskowymi (pogoda, temperatura, rodzaj stadionu itp.) wszystkich meczów, które również mogą mieć wpływ na kontuzję.

W przeciwieństwie do standardowych konkursów na Kaggle, NFL nie oczekuje modeli predykcyjnych z najlepszymi wynikami, tylko analizy danych.

ANALIZA DANYCH

Zaczynamy od podstawowych bibliotek Pythona używanych do ML i wizualizacji danych.

import numpy as np
import pandas as pd

import matplotlib.patches as patches
import matplotlib.pyplot as plt
%matplotlib inline

Najpierw wgrywamy dane z pierwszego zbioru ‘InjuryRecord’. W zbiorze mamy dane 105 odniesionych kontuzji, czyli niektórzy gracze byli kontuzjowani więcej niż jeden raz.

injury_df = pd.read_csv('InjuryRecord.csv')
injury_df.head()
#injury_df.info()

Przekazane dane nie są kompletne. Brakuje 28 rekordów ‘PlayKey’. ‘PlayKey’ to oznaczenie konkretnej zagrywki (‘play’) w danym meczu. Dla 28 ze 105 odniesionych kontuzji nie mamy informacji o zagrywce, w której doszło do tych kontuzji.

for feature in injury_df.columns:
    print(feature,'Number of missing values: ', injury_df[feature].isnull().sum())
    print('-------------------------------------')

Uzupełniamy brakujące dane wykorzystując kolumnę ‘GameID’ i dodając do niej string ‘-1’. Zakładamy, oczywiście błędnie, że wszystkie te kontuzje wydarzyły się w pierwszej zagrywce meczu. Przy tak małej ilości danych warto iść na jakieś kompromisy:).

injury_df['new']='-1'
injury_df['PlayKey'].fillna(injury_df['GameID']+injury_df['new'], inplace=True)
del injury_df['new']

Rozkład ilości kontuzji w zależności od rodzaju kontuzjowanej części ciała

plt.subplots_adjust(wspace = 3)
plt.subplot(1,2,1)
injury_df['BodyPart'].value_counts().plot(kind='bar', title='Distribution of injuries by BodyPart:', figsize=(10,5))
#normalized distribution
plt.subplot(1,2,2)
injury_df['BodyPart'].value_counts(normalize=True).plot(kind='bar', title='Normalized distribution of injuries by BodyPart:', figsize=(10,5))
plt.show()

Rozkład ilości kontuzji w zależności od rodzaju nawierzchni (zauważamy, że więcej kontuzji jest na sztucznej nawierzchni):

plt.subplot(1,2,1)
injury_df['Surface'].value_counts().plot(kind='bar', title='Distribution of injuries by Surface type:', figsize=(10,5))
#normalized distribution
plt.subplot(1,2,2)
injury_df['Surface'].value_counts(normalize=True).plot(kind='bar', \
            title='Normalized distribution of injuries by Surface type:', figsize=(10,5))
plt.show()
plt.tight_layout

Wizualizacja obu powyższych wykresów na jednym wykresie słupkowym (wyraźnie więcej kontuzji kostki (ankle) na nawierzchni syntetycznej):

injury_df.groupby(['BodyPart', 'Surface']).count().unstack('BodyPart')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by BodyPart and SurfaceType ')

Kolumny ‘DM_M1’, ‘DM_M7’, ‘DM_M28’, ‘DM_M42’ za pomocą 0 i 1 opisują jak poważna jest kontuzja. Np. ‘1’ w kolumnie ‘DM_M7’ oznacza, że gracz stracił kolejne 7 dni lub więcej na wyleczenie kontuzji. Na potrzeby wizualizacji pogrupujemy te dane w bardziej przystępne, opisowe kategorie.

def map_injury_level(x):
    if x == 1: return 'light'
    if x == 2: return 'medium'
    if x == 3: return 'severe'
    if x == 4: return 'very severe'
    return 'unknown'

injury_df['InjuryLevel']=injury_df[['DM_M1','DM_M7','DM_M28','DM_M42']].sum(axis=1)
injury_df['InjuryLevel_cat']=injury_df['InjuryLevel'].map(map_injury_level)
injury_df.head(10)

Rozkład ilości kontuzji w zależności od stopnia urazu i kontuzjowanej części ciała:

injury_df.groupby(['InjuryLevel_cat','BodyPart']).count().unstack('InjuryLevel_cat')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by BodyPart and InjuryLevel: ', stacked=True)

Rozkład ilości kontuzji w zależności od stopnia urazu i rodzaju nawierzchni:

injury_df.groupby(['InjuryLevel_cat','Surface']).count().unstack('InjuryLevel_cat')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by Surface and InjuryLevel: ', stacked=True)

Zgodnie z wcześniejszą analizą więcej kontuzji miało miejsce na sztucznej nawierzchni, ale musimy to skonfrontować z rozkładem wszystkich meczy. Jeżeli okaże się, że większość meczów rozgrywana była na sztucznej murawie, wtedy sam rodzaj murawy nie będzie czynnikiem kontuzjogennym. Aby to sprawdzić musimy wczytać kolejny zbiór danych ‘PlayList.csv’.

play_list_df = pd.read_csv('PlayList.csv')
play_list_df.head()
#play_list_df.info()

Okazuje się, że większość meczy była rozgrywana na naturalnej murawie.

play_list_df[['GameID','FieldType']].groupby('GameID').min()['FieldType'].value_counts()

Poniżej wykres pokazujący prawdopodobieństwo odniesienia kontuzji. Jak widać dla nawierzchni syntetycznej jest ono znacznie większe.

inj_ratio = injury_df['Surface'].value_counts()/play_list_df[['GameID','FieldType']].groupby('GameID').\
    min()['FieldType'].value_counts()
inj_ratio.plot(kind='bar', title='Injury probability by surface type:')

Analiza danych w zbiorze ‘PlayList’

unique_players_nr = play_list_df['PlayerKey'].nunique()
unique_players_nr
unique_games_nr = play_list_df['GameID'].nunique()
unique_games_nr
unique_plays_nr = play_list_df['PlayKey'].nunique()
unique_plays_nr
print('There are {} players in the dataset.'.format(unique_players_nr))
print('There are {} games in the dataset.'.format(unique_games_nr))
print('There are {} plays in the dataset.'.format(unique_plays_nr))

Rozkład zagrywek w zależności od ich rodzaju. Dominują dwa rodzaje zagrań: ‘Pass’ i ‘Rush’

plt.subplot(1,2,1)
play_list_df['PlayType'].value_counts().plot(kind='bar', title='Distribution of plays by PlayType:', figsize=(10,5))
#normalized
plt.subplot(1,2,2)
play_list_df['PlayType'].value_counts(normalize=True).plot(kind='bar', \
        title='Normalized distribution of plays by PlayType:', figsize=(10,5))
plt.show()

Dane ‘StadiumType’ zawierają wiele literówek i podobnych opisów, które znaczą to samo.

play_list_df['StadiumType'].value_counts()

Aby dane były czytelniejsze pogrupowałem je w sześć typów: outdoor, indoor_closed, indoor_open, dome_closed, dome_open, unknown.

def map_stadium_type(x):
    if x in ['Outdoor', 'Outdoors', 'Cloudy', 'Heinz Field', 
              'Outdor', 'Ourdoor', 'Outside', 'Outddors', 
              'Outdoor Retr Roof-Open', 'Oudoor', 'Bowl']: return 'outdoor'
    if x in ['Indoors', 'Indoor', 'Indoor, Roof Closed', 'Indoor, Roof Closed',
                   'Retractable Roof', 'Retr. Roof-Closed', 'Retr. Roof - Closed', 'Retr. Roof Closed']: return 'indoor_closed'
    if x in ['Indoor, Open Roof', 'Open', 'Retr. Roof-Open', 'Retr. Roof - Open']: return 'indoor_open'
    if x in ['Dome', 'Domed, closed', 'Closed Dome', 'Domed', 'Dome, closed']: return 'dome_closed'
    if x in ['Domed, Open', 'Domed, open']: return 'dome_open'
    return 'unknown'

play_list_df['StadiumType_cat']=play_list_df['StadiumType'].map(lambda x: map_stadium_type(x))
play_list_df['StadiumType_cat'].value_counts()

Rozkład zagrywek w zależności od rodzaju stadionu. Zdecydowana większość zagrywek była na otwartych arenach.

plt.subplot(1,2,1)
play_list_df['StadiumType_cat'].value_counts().plot(kind='bar', \
        title='Distribution of plays by StadiumType:', figsize=(10,5))
#normalized
plt.subplot(1,2,2)
play_list_df['StadiumType_cat'].value_counts(normalize=True).plot(kind='bar', \
        title='Normalized distribution of plays by StadiumType:', figsize=(10,5))
plt.show()

Na potrzeby dalszych wizualizacji do zbioru ‘InjuryRecord’ dołączamy zbiór ‘PlayList’. Łączymy dane w jeden dataframe.

injury_df_ext = injury_df.merge(play_list_df)
injury_df_ext.head()
#injury_df_ext.info()

Rozkład ilości kontuzji w zależności od pozycji gracza – ‘RosterPosition’. Zgodnie z opisem na Kaggle ‘RosterPosition’ może być inne niż ‘Position’. Szczerze mówiąc za mało wiem o futbolu amerykańskim, żeby wychwycić różnicę, więc pokazuję oba wykresy:)

injury_df_ext['RosterPosition'].value_counts().plot(kind='bar', title='Distribution of injuries by RosterPosition:')

Rozkład ilości kontuzji w zależności od pozycji gracza – ‘Position’

injury_df_ext['Position'].value_counts().plot(kind='bar', title='Distribution of injuries by Position:')

Rozkład ilości kontuzji w zależności od rodzaju zagrywki.

injury_df_ext['PlayType'].value_counts().plot(kind='bar', title='Distribution of injuries by PlayType:')

Rozkład ilości kontuzji w zależności od ‘BodyPart’ i ‘RosterPosition’ na jednym wykresie.

injury_df_ext.groupby(['RosterPosition','BodyPart']).count().unstack('BodyPart')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by BodyPart and RosterPosition', stacked=True)

Analogiczny wykres dla ‘BodyPart’ i ‘Position’

injury_df_ext.groupby(['Position','BodyPart']).count().unstack('BodyPart')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by BodyPart and Position', stacked=True)

Rozkład ilości kontuzji w zależności od rodzaju kontuzji i rodzaju zagrywki

injury_df_ext.groupby(['PlayType','BodyPart']).count().unstack('BodyPart')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by BodyPart and PlayType', stacked=True)

Rozkład ilości kontuzji w zależności od rodzaju nawierzchni i pozycji gracza na boisku

injury_df_ext.groupby(['RosterPosition','Surface']).count().unstack('Surface')['PlayerKey']\
    .plot(kind='bar', title='Distribution of injuries by Surface type and RosterPosition', stacked=True)

Sprawdźmy też czy długość trwania meczu wpływa na ilość kontuzji. Hipoteza do weryfikacji brzmi: czy prawdopodobieństwo kontuzji w późniejszych rozgrywkach meczu jest większe (np. zawodnicy są już zmęczeni i bardziej podatni na uraz). Mediana kolumny ‘PlayerGamePlay’ wynosi 13. Czyli połowa kontuzji zdarza się przed 13tą zagrywką w meczu, w połowa po 13tej zagrywce.

injury_df_ext['PlayerGamePlay'].median()

Średnia ilość zagrywek w meczu dla wszystkich zawodników to ok. 46.

play_list_df[['GameID','PlayerGamePlay']].groupby('GameID').max()['PlayerGamePlay'].mean()

Zobaczmy rozkład zagrywek, w których dochodzi do kontuzji w zależności od rodzaju nawierzchni. Rozkład jest dla obu nawierzchni jest podobny, chociaż mediana dla nawierzchni syntetycznej jest większa niż dla naturalnej (ciekawe).

inj=injury_df_ext[['FieldType', 'PlayerGamePlay']]
inj1=inj[inj['FieldType']=='Synthetic']
print('Median time of injury on synthetic surface is {} play of the game:'.format(inj1['PlayerGamePlay'].median()))
inj1.plot(kind='box', title='Synthetic surface')
plt.show()

inj2= inj[inj['FieldType']=='Natural']
print('Median time of injury on natural surface is {} play of the game:'.format(inj2['PlayerGamePlay'].median()))
inj2.plot(kind='box', title='Natural surface')
plt.show()

Kolejna hipoteza: czy prawdopodobieństwo kontuzji zmienia się w trakcie trwania sezonu? Na poniższym wykresie widzimy, że im dalej w sezonie tym mniej kontuzji.

nr_injuries=injury_df_ext.groupby('PlayerGame').count()['PlayerKey']
nr_injuries.plot(kind='bar', title='Number of injuries by number of games played during the season:')

Ale ilość graczy biorących udział w meczach też się zmniejsza w trakcie sezonu:

nr_plays = play_list_df[['PlayerKey','PlayerGame']].drop_duplicates().groupby('PlayerGame').count()['PlayerKey']
nr_plays.plot(kind='bar', title='Number of players by number of games played during the season:')

Wykres przedstawiający procentowe prawdopodobieństwo kontuzji w przeciągu sezonu. Puste słupki oznaczają brak kontuzji.

injury_ratio_percent = (nr_injuries/nr_plays*100)
injury_ratio_percent.plot(kind='bar', title='Injury rate through season games [%]:')

Wpływ pogody na ilość kontuzji. Najpierw musimy pokategoryzować dane.

def map_weather_type(x):
    if x in ['30% Chance of Rain', 'Rainy', 'Rain Chance 40%', 'Showers', 'Cloudy, 50% change of rain', 'Rain likely, temps in low 40s.',
          'Cloudy with periods of rain, thunder possible. Winds shifting to WNW, 10-20 mph.',
          'Scattered Showers', 'Cloudy, Rain', 'Rain shower', 'Light Rain', 'Rain']: return 'rain'
    if x in ['Party Cloudy', 'Cloudy, chance of rain','Coudy',
              'Cloudy and cold', 'Cloudy, fog started developing in 2nd quarter',
              'Partly Clouidy', 'Mostly Coudy', 'Cloudy and Cool',
              'cloudy', 'Partly cloudy', 'Overcast', 'Hazy', 'Mostly cloudy', 'Mostly Cloudy',
              'Partly Cloudy', 'Cloudy']: return 'cloud'
    if x in ['Partly clear', 'Sunny and clear', 'Sun & clouds', 'Clear and Sunny',
           'Sunny and cold', 'Sunny Skies', 'Clear and Cool', 'Clear and sunny',
           'Sunny, highs to upper 80s', 'Mostly Sunny Skies', 'Cold',
           'Clear and warm', 'Sunny and warm', 'Clear and cold', 'Mostly sunny',
           'T: 51; H: 55; W: NW 10 mph', 'Clear Skies', 'Clear skies', 'Partly sunny',
           'Fair', 'Partly Sunny', 'Mostly Sunny', 'Clear', 'Sunny']: return 'sun'
    if x in ['Cloudy, light snow accumulating 1-3"', 'Heavy lake effect snow', 'Snow']: return 'snow'
    if x in ['N/A Indoor', 'Indoors', 'Indoor', 'N/A (Indoors)', 'Controlled Climate']: return 'indoor'
    return 'unknown'

injury_df_ext['Weather_cat']=injury_df_ext['Weather'].map(lambda x: map_weather_type(x))
injury_df_ext['Weather_cat'].value_counts().plot(kind='bar', title='Number of injuries by weather conditions:')

Ilość meczy w danych warunkach pogodowych.

play_list_df['Weather_cat']=play_list_df['Weather'].map(lambda x: map_weather_type(x))
play_list_df[['PlayerKey','PlayerGame','Weather_cat']].drop_duplicates()\
['Weather_cat'].value_counts().plot(kind='bar', title='Number of all games by weather conditions:')

Rozkład ilości urazów (dane procentowe) w zależności od pogody. Widzimy, że nie było żadnej kontuzji, gdy padał śnieg:)

inj_weather = injury_df_ext['Weather_cat'].value_counts()
allgames_weather = play_list_df[['PlayerKey','PlayerGame','Weather_cat']].drop_duplicates()['Weather_cat'].value_counts()
inj_weather_ratio_percent = (inj_weather/allgames_weather*100)
inj_weather_ratio_percent.plot(kind='bar', title='Injury rate by weather [%]:')

Ostatni zbiór danych, czy ‘PlayerTrackData’. Jest to potężna dawka danych (ponad 3 GB) pokazująca dokładne położenie 250 graczy w trakcie meczu (rodzaj zagrywki, współrzędne na boisku, prędkość, kierunek ruchu zawodnika oraz kierunek, w którym skierowana jest twarz zawodnika).

player_track_data_df = pd.read_csv('PlayerTrackData.csv')
player_track_data_df.head(10)
#player_track_data_df.info()

Uzupełniam brakujące dane w kolumnie ‘event’ kopiując istniejące opisy.

player_track_data_df['event'].fillna(method='ffill', inplace=True)

Kolumna ‘event’ posiada 80 różnych rodzajów zagrywek.

player_track_data_df['event'].nunique()
#player_track_data_df['event'].unique()

Nie znam się dokładnie na futbolu amerykańskim, więc bardzo zgrubnie podzielę zagrywki na dwie kategorie: ‘in_play’ gdy zagrywka była z udziałem piłki, ‘not_in_play’ gdy zagrywka była bez piłki. Na wykresie rozkładu widać, że obie kategorie są zbliżone pod względem ilości zagrywek.

def map_event_type(x):
    if x in ["tackle", "ball_snap", "pass_outcome_incomplete", "out_of_bounds", "first_contact", "handoff", 
             "pass_forward", "pass_outcome_caught", "touchdown", "qb_sack", "touchback", "kickoff", "punt", 
             "pass_outcome_touchdown", "pass_arrived", "extra_point", "field_goal", "play_action", "kick_received", 
             "fair_catch", "punt_downed", "run", "punt_received", "qb_kneel", "pass_outcome_interception", 
             "field_goal_missed", "fumble", "fumble_defense_recovered", "qb_spike", "extra_point_missed", 
             "fumble_offense_recovered", "pass_tipped", "lateral", "qb_strip_sack", "safety", "kickoff_land", 
             "snap_direct", "kick_recovered", "field_goal_blocked", "punt_muffed", "pass_shovel", "extra_point_blocked", 
             "pass_lateral", "punt_blocked", "run_pass_option", "free_kick", "punt_fake", "end_path", "drop_kick", 
             "field_goal_fake", "extra_point_fake", "xp_fake"]: return 'in_play'
    return 'not_in_play'

player_track_data_df['in_play']=player_track_data_df['event'].map(lambda x: map_event_type(x))
#we can see that categorization we created separated the data into similar size categories
player_track_data_df['in_play'].value_counts().plot(kind='bar', title='Number of events with and without ball in play:')

Dodajmy nowe dane do poprzednich, aby stworzyć jeden dataframe. Dodatkowo do w nowym dataframe tworzę nową kolumnę ‘IsInjured’, która przyjmuje wartość True, gdy doszło do kontuzji i False, gdy kontuzja się nie przydarzyła.

track_injury_df = player_track_data_df.merge(injury_df, how='left')
#and create new boolean column ['IsInjured']
track_injury_df['IsInjured']=track_injury_df['PlayerKey']>0

Aby zmniejszyć zużycie pamięci usuwamy dane, które nie są już nam potrzebne.

blacklist = ['PlayerKey', 'GameID', 'BodyPart', 'Surface', 'DM_M1', 'DM_M7',
       'DM_M28', 'DM_M42', 'InjuryLevel', 'InjuryLevel_cat']
for feat in blacklist:
    del track_injury_df[feat]

Na potrzeby przyszłej predykcji dodajemy jeszcze jedną kolumnę ‘IsInjured_num’, która przyjmuje wartości 1 dla kontuzji i 0 dla braku kontuzji.

track_injury_df['IsInjured_num']=track_injury_df['IsInjured'].map(lambda x: int(x))
track_injury_df.head()

Lokalnie zapisujemy wszystkie nowo utworzone datasety do plików, aby mieć je już gotowe do dalszej pracy. To może chwilę potrwać:)

injury_df_ext.to_csv('injury_df_ext.csv', index=False)
play_list_df.to_csv('play_list_df.csv', index=False)
track_injury_df.to_csv('track_injury_df.csv', index=False)

Na koniec ffunkcja do wizualizacji boiska NFL i ruchu graczy, której autorem jest Rob Mulla (szczegóły możecie znaleźć tutaj:

https://www.kaggle.com/robikscube/nfl-big-data-bowl-plotting-player-position

def create_football_field(linenumbers=True,
                          endzones=True,
                          highlight_line=False,
                          highlight_line_number=50,
                          highlighted_name='Line of Scrimmage',
                          fifty_is_los=False,
                          figsize=(12, 6.33)):
    """
    Function that plots the football field for viewing plays.
    Allows for showing or hiding endzones.
    """
    rect = patches.Rectangle((0, 0), 120, 53.3, linewidth=0.1,
                             edgecolor='r', facecolor='darkgreen', zorder=0)

    fig, ax = plt.subplots(1, figsize=figsize)
    ax.add_patch(rect)

    plt.plot([10, 10, 10, 20, 20, 30, 30, 40, 40, 50, 50, 60, 60, 70, 70, 80,
              80, 90, 90, 100, 100, 110, 110, 120, 0, 0, 120, 120],
             [0, 0, 53.3, 53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3,
              53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3, 53.3, 53.3, 0, 0, 53.3],
             color='white')
    if fifty_is_los:
        plt.plot([60, 60], [0, 53.3], color='gold')
        plt.text(62, 50, '<- Player Yardline at Snap', color='gold')
    # Endzones
    if endzones:
        ez1 = patches.Rectangle((0, 0), 10, 53.3,
                                linewidth=0.1,
                                edgecolor='r',
                                facecolor='blue',
                                alpha=0.2,
                                zorder=0)
        ez2 = patches.Rectangle((110, 0), 120, 53.3,
                                linewidth=0.1,
                                edgecolor='r',
                                facecolor='blue',
                                alpha=0.2,
                                zorder=0)
        ax.add_patch(ez1)
        ax.add_patch(ez2)
    plt.xlim(0, 120)
    plt.ylim(-5, 58.3)
    plt.axis('off')
    if linenumbers:
        for x in range(20, 110, 10):
            numb = x
            if x > 50:
                numb = 120 - x
            plt.text(x, 5, str(numb - 10),
                     horizontalalignment='center',
                     fontsize=20,  # fontname='Arial',
                     color='white')
            plt.text(x - 0.95, 53.3 - 5, str(numb - 10),
                     horizontalalignment='center',
                     fontsize=20,  # fontname='Arial',
                     color='white', rotation=180)
    if endzones:
        hash_range = range(11, 110)
    else:
        hash_range = range(1, 120)

    for x in hash_range:
        ax.plot([x, x], [0.4, 0.7], color='white')
        ax.plot([x, x], [53.0, 52.5], color='white')
        ax.plot([x, x], [22.91, 23.57], color='white')
        ax.plot([x, x], [29.73, 30.39], color='white')

    if highlight_line:
        hl = highlight_line_number + 10
        plt.plot([hl, hl], [0, 53.3], color='yellow')
        plt.text(hl + 2, 50, '<- {}'.format(highlighted_name),
                 color='yellow')
    return fig, ax

Wizualizacja ścieżki jednego przykładowego gracza, który odniósł kontuzję.

example_play_id = injury_df_ext['PlayKey'].values[16]
fig, ax = create_football_field()
track_injury_df[track_injury_df['PlayKey']==example_play_id].plot(kind='scatter', x='x', y='y', ax=ax, color='orange')
plt.show()

Wizualizacja wszystkich kontuzjowanych graczy nie pomaga wyciągnąć żadnych konkretnych wniosków, ale fajnie wygląda:)

inj_play_list = injury_df_ext['PlayKey'].tolist()
fig, ax = create_football_field()
for playkey, inj_play in player_track_data_df.query('PlayKey in @inj_play_list').groupby('PlayKey'):
    inj_play.plot(kind='scatter', x='x', y='y', ax=ax, color='orange', alpha=0.2)
plt.show()

Na pewno z danych PlayerTrackData można wyciągnąć jeszcze wiele informacji i przeanalizować je pod kątem wpływu chociażby prędkości czy przyspieszenia zawodników na ilość kontuzji. W następnym wpisie chciałbym zbudować model, który będzie przewidywał prawdopodobieństwo wystąpienia kontuzji w oparciu o dostępne dane. Wszelkie komentarze do powyższej analizy są bardzo mile widziane:)

Comments

comments

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.