def question1(): # set up initial path to data DATADIR = '/Users/Kafka/PycharmProjects/analytics/metrica/' game_id = 2 # let's look at sample match 2 # read in the event data events = mio.read_event_data(DATADIR, game_id) # unit conversion events = mio.to_metric_coordinates(events) # Get all shots shots = events[events['Type'] == 'SHOT'] # Get the shots that led to a goal all_goals = shots[shots['Subtype'].str.contains('-GOAL')].copy() # Add a column event 'Minute' to the data frame all_goals['Minute'] = all_goals['Start Time [s]'] / 60. # Plotting pitch fig, ax = mviz.plot_pitch() # point of shot ax.plot(events.loc[823]['Start X'], events.loc[823]['Start Y'], 'ro') # negative as it is for the away team and it make it easier to differentiate ax.plot(-events.loc[1118]['Start X'], -events.loc[1118]['Start Y'], 'bo') # Shot direction ax.annotate("", xy=events.loc[823][['End X', 'End Y']], xytext=events.loc[823][['Start X', 'Start Y']], alpha=0.6, arrowprops=dict(arrowstyle="->", color='r')) ax.annotate("", xy=-events.loc[1118][['End X', 'End Y']], xytext=-events.loc[1118][['Start X', 'Start Y']], alpha=0.6, arrowprops=dict(arrowstyle="->", color='b')) # plot passing move in run up to goal mviz.plot_events(events.loc[818:822], indicators=['Marker', 'Arrow'], annotate=True, figax=(fig, ax)) mviz.plot_events(events.loc[1109:1111], indicators=['Marker', 'Arrow'], color='b', annotate=True, neg=True, figax=(fig, ax)) mviz.plot_events(events.loc[1115:1116], indicators=['Marker', 'Arrow'], color='b', annotate=True, neg=True, figax=(fig, ax)) fig.savefig('question1.pdf', dpi=100) fig.show()
def question2(): # set up initial path to data DATADIR = '/Users/Kafka/PycharmProjects/analytics/metrica/' game_id = 2 # let's look at sample match 2 # read in the event data events = mio.read_event_data(DATADIR, game_id) # unit conversion events = mio.to_metric_coordinates(events) # Get all shots shots = events[events['Type'] == 'SHOT'] # Get the shots that led to a goal all_goals = shots[shots['Subtype'].str.contains('-GOAL')].copy() # Add a column event 'Minute' to the data frame all_goals['Minute'] = all_goals['Start Time [s]'] / 60. # Plotting pitch fig, ax = mviz.plot_pitch() for i in shots.index[shots['From'] == 'Player9'].tolist(): if 'OFF' in events.loc[i]['Subtype']: arr = "-[, widthB=.4" clr = 'r' else: arr = "->" clr = 'b' ax.plot(events.loc[i]['Start X'], events.loc[i]['Start Y'], 'ro') # Shot direction ax.annotate("", xy=events.loc[i][['End X', 'End Y']], xytext=events.loc[i][['Start X', 'Start Y']], alpha=0.6, arrowprops=dict(arrowstyle=arr, color=clr)) fig.savefig('question2.pdf', dpi=100) fig.show()
def question3(): # set up initial path to data DATADIR = '/Users/Kafka/PycharmProjects/analytics/metrica/' game_id = 2 # let's look at sample match 2 # read in the event data events = mio.read_event_data(DATADIR, game_id) # unit conversion events = mio.to_metric_coordinates(events) # Plot some player trajectories (players 11,1,2,3,4) fig, ax = mviz.plot_pitch() goal_event_id = 1118 # READING IN TRACKING DATA tracking_home = mio.tracking_data(DATADIR, game_id, 'Home') tracking_away = mio.tracking_data(DATADIR, game_id, 'Away') # Convert positions from metrica units to meters tracking_home = mio.to_metric_coordinates(tracking_home) tracking_away = mio.to_metric_coordinates(tracking_away) # PLOT POISTIONS AT GOAL fig, ax = mviz.plot_events(events.loc[goal_event_id:goal_event_id], indicators=['Marker', 'Arrow'], annotate=True) goal_frame = events.loc[goal_event_id]['Start Frame'] fig, ax = mviz.plot_frame(tracking_home.loc[goal_frame], tracking_away.loc[goal_frame], figax=(fig, ax)) fig.savefig('question3.pdf', dpi=100) fig.show()
def question4(): # set up initial path to data DATADIR = '/Users/Kafka/PycharmProjects/analytics/metrica/' game_id = 2 # let's look at sample match 2 # READING IN TRACKING DATA tracking_home = mio.tracking_data(DATADIR, game_id, 'Home') tracking_away = mio.tracking_data(DATADIR, game_id, 'Away') # Convert positions from metrica units to meters tracking_home = mio.to_metric_coordinates(tracking_home) tracking_away = mio.to_metric_coordinates(tracking_away) # difference for every row in the first half absolute value and summed. Then added to the second half data. home_tot_distance = tracking_home[tracking_home['Period'] == 1].diff().abs( ).sum() + tracking_home[tracking_home['Period'] == 2].diff().abs().sum() # Removing unnecessary columns home_tot_distance = home_tot_distance.drop( ['Period', 'Time [s]', 'ball_x', 'ball_y']) # seperating x and y coords for each player and zipping the values together and summing. # This could be made more efficient. home_tot_distance = list( zip(home_tot_distance[['_y' in s for s in home_tot_distance.index]], home_tot_distance[['_x' in s for s in home_tot_distance.index]])) home_tot_distance = [x + y for (x, y) in home_tot_distance] away_tot_distance = tracking_away[tracking_away['Period'] == 1].diff().abs( ).sum() + tracking_away[tracking_away['Period'] == 2].diff().abs().sum() away_tot_distance = away_tot_distance.drop( ['Period', 'Time [s]', 'ball_x', 'ball_y']) # seperating x and y coords for each player and zipping the values together and summing. # This could be made more efficient. away_tot_distance = list( zip(away_tot_distance[['_y' in s for s in away_tot_distance.index]], away_tot_distance[['_x' in s for s in away_tot_distance.index]])) away_tot_distance = [x + y for (x, y) in away_tot_distance]
def calculate_epv_added(event_id, events, tracking_home, tracking_away, GK_numbers, EPV, params): """ calculate_epv_added Calculates the expected possession value added by a pass Parameters ----------- event_id: Index (not row) of the pass event to calculate EPV-added score events: Dataframe containing the event data tracking_home: tracking DataFrame for the Home team tracking_away: tracking DataFrame for the Away team GK_numbers: tuple containing the player id of the goalkeepers for the (home team, away team) EPV: tuple Expected Possession value grid (loaded using load_EPV_grid() ) params: Dictionary of pitch control model parameters (default model parameters can be generated using default_model_params() ) Returrns ----------- EEPV_added: Expected EPV value-added of pass defined by event_id EPV_difference: The raw change in EPV (ignoring pitch control) between end and start points of pass """ # pull out pass details from the event data pass_start_pos = np.array( [events.loc[event_id]['Start X'], events.loc[event_id]['Start Y']]) pass_target_pos = np.array( [events.loc[event_id]['End X'], events.loc[event_id]['End Y']]) pass_frame = events.loc[event_id]['Start Frame'] pass_team = events.loc[event_id].Team # direction of play for atacking team (so we know whether to flip the EPV grid) home_attack_direction = mio.find_playing_direction(tracking_home, 'Home') if pass_team == 'Home': attack_direction = home_attack_direction attacking_players = mpc.initialise_players( tracking_home.loc[pass_frame], 'Home', params, GK_numbers[0]) defending_players = mpc.initialise_players( tracking_away.loc[pass_frame], 'Away', params, GK_numbers[1]) elif pass_team == 'Away': attack_direction = home_attack_direction * -1 defending_players = mpc.initialise_players( tracking_home.loc[pass_frame], 'Home', params, GK_numbers[0]) attacking_players = mpc.initialise_players( tracking_away.loc[pass_frame], 'Away', params, GK_numbers[1]) # flag any players that are offside attacking_players = mpc.check_offsides(attacking_players, defending_players, pass_start_pos, GK_numbers) # pitch control grid at pass start location Patt_start, _ = mpc.calculate_pitch_control_at_target( pass_start_pos, attacking_players, defending_players, pass_start_pos, params) # pitch control grid at pass end location Patt_target, _ = mpc.calculate_pitch_control_at_target( pass_target_pos, attacking_players, defending_players, pass_start_pos, params) # EPV at start location EPV_start = get_EPV_at_location(pass_start_pos, EPV, attack_direction=attack_direction) # EPV at end location EPV_target = get_EPV_at_location(pass_target_pos, EPV, attack_direction=attack_direction) # 'Expected' EPV at target and start location EEPV_target = Patt_target * EPV_target EEPV_start = Patt_start * EPV_start # difference is the (expected) EPV added EEPV_added = EEPV_target - EEPV_start # Also calculate the straight up change in EPV EPV_difference = EPV_target - EPV_start return EEPV_added, EPV_difference
def find_max_value_added_target(event_id, events, tracking_home, tracking_away, GK_numbers, EPV, params): """ find_max_value_added_target Finds the *maximum* expected possession value that could have been achieved for a pass (defined by the event_id) by searching the entire field for the best target. Parameters ----------- event_id: Index (not row) of the pass event to calculate EPV-added score events: Dataframe containing the event data tracking_home: tracking DataFrame for the Home team tracking_away: tracking DataFrame for the Away team GK_numbers: tuple containing the player id of the goalkeepers for the (home team, away team) EPV: tuple Expected Possession value grid (loaded using load_EPV_grid() ) params: Dictionary of pitch control model parameters (default model parameters can be generated using default_model_params() ) Returrns ----------- maxEPV_added: maximum EPV value-added that could be achieved at the current instant max_target_location: (x,y) location of the position of the maxEPV_added """ # pull out pass details from the event data pass_start_pos = np.array( [events.loc[event_id]['Start X'], events.loc[event_id]['Start Y']]) pass_frame = events.loc[event_id]['Start Frame'] pass_team = events.loc[event_id].Team # direction of play for atacking team (so we know whether to flip the EPV grid) home_attack_direction = mio.find_playing_direction(tracking_home, 'Home') if pass_team == 'Home': attack_direction = home_attack_direction attacking_players = mpc.initialise_players( tracking_home.loc[pass_frame], 'Home', params, GK_numbers[0]) defending_players = mpc.initialise_players( tracking_away.loc[pass_frame], 'Away', params, GK_numbers[1]) elif pass_team == 'Away': attack_direction = home_attack_direction * -1 defending_players = mpc.initialise_players( tracking_home.loc[pass_frame], 'Home', params, GK_numbers[0]) attacking_players = mpc.initialise_players( tracking_away.loc[pass_frame], 'Away', params, GK_numbers[1]) # flag any players that are offside attacking_players = mpc.check_offsides(attacking_players, defending_players, pass_start_pos, GK_numbers) # pitch control grid at pass start location Patt_start, _ = mpc.calculate_pitch_control_at_target( pass_start_pos, attacking_players, defending_players, pass_start_pos, params) # EPV at start location EPV_start = get_EPV_at_location(pass_start_pos, EPV, attack_direction=attack_direction) # calculate pitch control surface at moment of the pass PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id, events, tracking_home, tracking_away, params, GK_numbers, field_dimen=( 106., 68., ), n_grid_cells_x=50, offsides=True) # EPV surface at instance of the pass if attack_direction == -1: EEPV = np.fliplr(EPV) * PPCF else: EEPV = EPV * PPCF # find indices of the maxEPV maxEPV_idx = np.unravel_index(EEPV.argmax(), EEPV.shape) # Expected EPV at current ball position EEPV_start = Patt_start * EPV_start # maxEPV_added (difference between max location and current ball location) maxEPV_added = EEPV.max() - EEPV_start # location of maximum max_target_location = (xgrid[maxEPV_idx[1]], ygrid[maxEPV_idx[0]]) return maxEPV_added, max_target_location
def plot_EPV_for_event(event_id, events, tracking_home, tracking_away, PPCF, EPV, alpha=0.7, include_player_velocities=True, annotate=False, autoscale=0.1, contours=False, field_dimen=(106.0, 68)): """ plot_EPV_for_event( event_id, events, tracking_home, tracking_away, PPCF, EPV, alpha, include_player_velocities, annotate, autoscale, contours, field_dimen) Plots the EPVxPitchControl surface at the instant of the event given by the event_id. Player and ball positions are overlaid. Parameters ----------- event_id: Index (not row) of the event that describes the instant at which the pitch control surface should be calculated events: Dataframe containing the event data tracking_home: (entire) tracking DataFrame for the Home team tracking_away: (entire) tracking DataFrame for the Away team PPCF: Pitch control surface (dimen (n_grid_cells_x,n_grid_cells_y) ) containing pitch control probability for the attcking team (as returned by the generate_pitch_control_for_event in Metrica_PitchControl) EPV: Expected Possession Value surface. EPV is the probability that a possession will end with a goal given the current location of the ball. The EPV surface is saved in the FoT github repo and can be loaded using Metrica_EPV.load_EPV_grid() alpha: alpha (transparency) of player markers. Default is 0.7 include_player_velocities: Boolean variable that determines whether player velocities are also plotted (as quivers). Default is False annotate: Boolean variable that determines with player jersey numbers are added to the plot (default is False) autoscale: If True, use the max of surface to define the colorscale of the image. If set to a value [0-1], uses this as the maximum of the color scale. field_dimen: tuple containing the length and width of the pitch in meters. Default is (106,68) Returrns ----------- fig,ax : figure and aixs objects (so that other data can be plotted onto the pitch) """ # pick a pass at which to generate the pitch control surface pass_frame = events.loc[event_id]['Start Frame'] pass_team = events.loc[event_id].Team # plot frame and event fig, ax = plot_pitch(field_color='white', field_dimen=field_dimen) plot_frame(tracking_home.loc[pass_frame], tracking_away.loc[pass_frame], figax=(fig, ax), PlayerAlpha=alpha, include_player_velocities=include_player_velocities, annotate=annotate) plot_events(events.loc[event_id:event_id], figax=(fig, ax), indicators=['Marker', 'Arrow'], annotate=False, color='k', alpha=1) # plot pitch control surface if pass_team == 'Home': cmap = 'Reds' lcolor = 'r' EPV = np.fliplr(EPV) if mio.find_playing_direction( tracking_home, 'Home') == -1 else EPV else: cmap = 'Blues' lcolor = 'b' EPV = np.fliplr(EPV) if mio.find_playing_direction( tracking_away, 'Away') == -1 else EPV EPVxPPCF = PPCF * EPV if autoscale is True: vmax = np.max(EPVxPPCF) * 2. elif autoscale >= 0 and autoscale <= 1: vmax = autoscale else: assert False, "'autoscale' must be either {True or between 0 and 1}" ax.imshow(np.flipud(EPVxPPCF), extent=(-field_dimen[0] / 2., field_dimen[0] / 2., -field_dimen[1] / 2., field_dimen[1] / 2.), interpolation='spline36', vmin=0.0, vmax=vmax, cmap=cmap, alpha=0.7) if contours: ax.contour(EPVxPPCF, extent=(-field_dimen[0] / 2., field_dimen[0] / 2., -field_dimen[1] / 2., field_dimen[1] / 2.), levels=np.array([0.75]) * np.max(EPVxPPCF), colors=lcolor, alpha=1.0) return fig, ax
Homework answers for lesson 4 of "Friends of Tracking" #FoT Data can be found at: https://github.com/metrica-sports/sample-data @author: Laurie Shaw (@EightyFivePoint) """ import LS_functions.Metrica_IO as mio import LS_functions.Metrica_Viz as mviz # set up initial path to data DATADIR = '/Users/Kafka/PycharmProjects/analytics/metrica' game_id = 2 # let's look at sample match 2 # read in the event data events = mio.read_event_data(DATADIR, game_id) # count the number of each event type in the data print(events['Type'].value_counts()) # Bit of housekeeping: unit conversion from metric data units to meters # What this really means is that the is the center spot. # Top right (x,y) = 53,34 # Bottom right (x,y) = 53,-34 # bottom left (x,y) = -53,-34 # Top right (x,y) = -53,34 events = mio.to_metric_coordinates(events) # Get events by team home_events = events[events['Team'] == 'Home'] away_events = events[events['Team'] == 'Away']