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 calculate_pitch_control_replaced_velocity( self, replace_x_velocity=0, replace_y_velocity=0, ): """ Function Description: This function calculates a pitch control surface after replacing a player's velocity vector with a new one. Ideally, this would be used to determine what space a player is gaining (and conceding) with his/her off the ball movement Input Parameters: :param float replace_x_velocity: The x vector of the velocity we would like to replace our given player with. Positive values will move the player toward's the home team's goal, while negative values will move the player towards the away team's goal. Measured in meters per second. Defaults to 0 :param float replace_y_velocity: The y vector of the velocity we would like to replace our given player with. Positive values will move the player to the left side of the pitch, from the perspective of the away team, while negative values will move the player towards the right side of the pitch from the perspective of the away team. Measured in meters per second. Defaults to 0. Returns: edited_pitch_control: Pitch control surface (dimen (n_grid_cells_x,n_grid_cells_y) ) containing pitch control probability for the attacking team with one player's velocity changed (defaulted to not moving). Surface for the defending team is simply 1-PPCFa. xgrid: Positions of the pixels in the x-direction (field length) ygrid: Positions of the pixels in the y-direction (field width) """ self._validate_inputs() # Determine which row in the tracking dataframe to use event_frame = self.events.loc[self.event_id]["Start Frame"] # Replace player's velocity datapoints with new velocity vector tmp_df_dict = self.df_dict.copy() for team, df in tmp_df_dict.items(): if team == self.team_player_to_analyze: df_tmp = df.copy() df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vx", ] = replace_x_velocity df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vy", ] = replace_y_velocity tmp_df_dict[team] = df_tmp edited_pitch_control, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id=self.event_id, events=self.events, df_dict=tmp_df_dict, params=self.params, field_dimen=self.field_dimens, n_grid_cells_x=self.n_grid_cells_x, ) return edited_pitch_control, xgrid, ygrid
def make_dataset(play, event_frame, metrica_attack, metrica_defence, bokeh_attack, bokeh_defence, field_dimen = (106.,68.,), new_grid_size = 500): params = mpc.default_model_params(3) event = events_df.loc[[(play, int(event_frame))]] tracking_frame = event['Start Frame'][0] att_frame = bokeh_attack.loc[(play,tracking_frame)] att_player_frame = att_frame[att_frame['player'] != "ball"] att_player_frame['Shirt Number'] = att_player_frame['player'].map(int).map(shirt_mapping[play]).fillna("") def_frame = bokeh_defence.loc[(play,tracking_frame)] def_player_frame = def_frame[def_frame['player'] != "ball"] def_player_frame['Shirt Number'] = def_player_frame['player'].map(int).map(shirt_mapping[play]).fillna("") ball_frame = att_frame[att_frame['player'] == "ball"] PPCF,xgrid,ygrid = pvm.lastrow_generate_pitch_control_for_event(play,event_frame, events_df, metrica_attack, metrica_defence, params, field_dimen = (106.,68.,), n_grid_cells_x = 50) PT = pvm.generate_relevance_at_event(play,event_frame, events_df, PPCF, params) PS = pvm.generate_scoring_opportunity(field_dimen = (106.,68.,),n_grid_cells_x = 50) PPV = pvm.generate_pitch_value(PPCF,PT,PS,field_dimen = (106.,68.,),n_grid_cells_x = 50) RPPV = pvm.generate_relative_pitch_value(play, event_frame, events_df, metrica_attack, PPV, xgrid, ygrid) xgrid_new = np.linspace( -field_dimen[0]/2., field_dimen[0]/2., new_grid_size) ygrid_new = np.linspace( -field_dimen[1]/2., field_dimen[1]/2., new_grid_size) PPCF_int = interpolate.interp2d(xgrid, ygrid, PPCF, kind = 'cubic') PPCF_new = PPCF_int(xgrid_new, ygrid_new) PPCF_dict = dict(image = [PPCF_new],x = [xgrid.min()],y = [ygrid.min()],dw = [field_dimen[0]], dh = [field_dimen[1]]) PT_int = interpolate.interp2d(xgrid, ygrid, PT, kind = 'cubic') PT_new = PT_int(xgrid_new, ygrid_new) PT_dict = dict(image = [PT_new],x = [xgrid.min()],y = [ygrid.min()],dw = [field_dimen[0]], dh = [field_dimen[1]]) PS_int = interpolate.interp2d(xgrid, ygrid, PS, kind = 'cubic') PS_new = PS_int(xgrid_new, ygrid_new) PS_dict = dict(image = [PS_new],x = [xgrid.min()],y = [ygrid.min()],dw = [field_dimen[0]], dh = [field_dimen[1]]) PPV_int = interpolate.interp2d(xgrid, ygrid, PPV, kind = 'cubic') PPV_new = PPV_int(xgrid_new, ygrid_new) PPV_dict = dict(image = [PPV_new],x = [xgrid.min()],y = [ygrid.min()],dw = [field_dimen[0]], dh = [field_dimen[1]]) RPPV_int = interpolate.interp2d(xgrid, ygrid, RPPV, kind = 'cubic') RPPV_new = RPPV_int(xgrid_new, ygrid_new) RPPV_dict = dict(image = [RPPV_new],x = [xgrid.min()],y = [ygrid.min()],dw = [field_dimen[0]], dh = [field_dimen[1]]) event_src = ColumnDataSource(event) att_src = ColumnDataSource(att_player_frame) def_src = ColumnDataSource(def_player_frame) ball_src = ColumnDataSource(ball_frame) PPCF_src = ColumnDataSource(PPCF_dict) PT_src = ColumnDataSource(PT_dict) PS_src = ColumnDataSource(PS_dict) PPV_src = ColumnDataSource(PPV_dict) RPPV_src = ColumnDataSource(RPPV_dict) return event_src, att_src, def_src, ball_src, PPCF_src, PT_src, PS_src, PPV_src, RPPV_src, xgrid, ygrid
def calculate_pitch_control_without_player(self): """ Function description: This function calculates a pitch control surface after removing the player from the pitch. This can be used to attribute which spaces on the pitch are controlled by the specific player, rather than the space occupied by his/her team. Returns: edited_pitch_control: Pitch control surface (dimen (n_grid_cells_x,n_grid_cells_y) ) containing pitch control probability for the attacking team after removing the relevant player from the pitch. Surface for the defending team is simply 1-PPCFa. xgrid: Positions of the pixels in the x-direction (field length) ygrid: Positions of the pixels in the y-direction (field width) """ self._validate_inputs() event_frame = self.events.loc[self.event_id]["Start Frame"] # Replace player's datapoint nan's, so pitch control does not take into account # the player when computing its surface tmp_df_dict = self.df_dict.copy() for team, df in tmp_df_dict.items(): if team == self.team_player_to_analyze: df_tmp = df.copy() df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_x", ] = np.nan df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_y", ] = np.nan df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vx", ] = np.nan df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vy", ] = np.nan tmp_df_dict[team] = df_tmp edited_pitch_control, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id=self.event_id, events=self.events, df_dict=tmp_df_dict, params=self.params, field_dimen=self.field_dimens, n_grid_cells_x=self.n_grid_cells_x, ) return edited_pitch_control, xgrid, ygrid
def calculate_epv_events_per_match(base_file, folder): """ Calculates the EPV and optimal EPV values for an input DataFrame of passes. Parameters ----------- base_file: input string corresponding to the match CSVs, from which we read the data folder: folder path Returns ----------- EPV_df: is the original events DataFrame with only the transition passes and includes the outcomes of the EPV calculations """ # make path string preprocessed_tracking_home_path = f'{folder}\\preprocessed\\{base_file}_tracking_home_processed.csv' preprocessed_tracking_away_path = f'{folder}\\preprocessed\\{base_file}_tracking_away_processed.csv' transition_passes_path = f'{folder}\\transition_passes\\{base_file}_transition_passes.csv' # load data tracking_home = pd.read_csv(preprocessed_tracking_home_path, index_col=0) tracking_away = pd.read_csv(preprocessed_tracking_away_path, index_col=0) events = pd.read_csv(transition_passes_path, index_col=0) # select only transition passes with an origin in the midfield: events = events[(events['Start X'] > -17.5) & (events['Start X'] < 17.5)] """ *** UPDATES TO THE MODEL: OFFSIDES """ # first get pitch control model parameters params = mpc.default_model_params() # find goalkeepers for offside calculation gk_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ] # get EPV surface epv = mepv.load_EPV_grid(DATADIR + '/EPV_grid.csv') # generate EPV values and append to events DataFrame epv_df = generate_epv_df(events, tracking_home, tracking_away, gk_numbers, epv, params) return epv_df
# Calculate player velocities # tracking_home = mvel.calc_player_velocities(tracking_home, smoothing=True) # tracking_away = mvel.calc_player_velocities(tracking_away, smoothing=True) #%% # **** NOTE ***** # if the lines above produce an error (happens for one version of numpy) change them to the lines below: # *************** tracking_home = mvel.calc_player_velocities(tracking_home, smoothing=True, filter_='moving_average') tracking_away = mvel.calc_player_velocities(tracking_away, smoothing=True, filter_='moving_average') """ *** UPDATES TO THE MODEL: OFFSIDES """ # first get pitch control model parameters params = mpc.default_model_params() # find goalkeepers for offside calculation GK_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ] #%% GK_numbers #%% """ *** GET EPV SURFACE **** """ home_attack_direction = mio.find_playing_direction( tracking_home, 'Home') # 1 if shooting left-right, else -1 #%% EPV = mepv.load_EPV_grid(DATADIR + '/EPV_grid.csv')
tracking_home, tracking_away, events) # Calculate player velocities #tracking_home = mvel.calc_player_velocities(tracking_home,smoothing=True) #tracking_away = mvel.calc_player_velocities(tracking_away,smoothing=True) # **** NOTE ***** # if the lines above produce an error (happens for one version of numpy) change them to the lines below: # *************** tracking_home = mvel.calc_player_velocities(tracking_home, smoothing=True, filter_='moving_average') tracking_away = mvel.calc_player_velocities(tracking_away, smoothing=True, filter_='moving_average') params = mpc.default_model_params(3) PPCF_actual, xgrid, ygrid = mpc.generate_pitch_control_for_event( 821, events, tracking_home, tracking_away, params, field_dimen=( 106., 68., ), n_grid_cells_x=50) mviz.plot_pitchcontrol_for_event(821, events, tracking_home,
def calculate_pitch_control_towards_goal(frame, team, event_id, events, tracking_home, tracking_away, column_name): """ Calculates the pitch control percentage of the tiles between the passer and the goal Parameters ----------- frame: frame of corresponding tracking row team: string 'Home' or 'Away' event_id: index corresponding to the passing event events: all transition pass events of this match tracking_home: tracking DataFrame for the Home team tracking_away: tracking DataFrame for the Away team column_name: the passing player's corresponding base column name for the tracking data Returns ----------- percentage_pitch_control: percentage of pitch control of the attacking team of the tile between the passer, and the goal """ # first get pitch control model parameters params = mpc.default_model_params() # find goalkeepers for offside calculation gk_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ] # evaluated pitch control surface for pass event PPCa, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id, events, tracking_home, tracking_away, params, gk_numbers, field_dimen=( 105., 68., ), n_grid_cells_x=50, offsides=True) # get playing_direction and x position of passing player if team == 'Home': player_x = tracking_home.loc[frame, column_name + '_x'] playing_direction = mio.find_playing_direction(tracking_home, team) # player_y = tracking_home.loc[frame, column_name + '_y'] else: player_x = tracking_away.loc[frame, column_name + '_x'] playing_direction = mio.find_playing_direction(tracking_away, team) # divide the playing field into grid cells n_grid_cells_x = 32 n_grid_cells_y = 50 field_dimen = [105, 68] # get grid cell of passing player dx = field_dimen[0] / n_grid_cells_x x_grid = np.arange(n_grid_cells_x) * dx - field_dimen[0] / 2. + dx / 2. # calculate the pitch control value of the grids towards goal, # and calculate the maximum pitch control for that area: if playing_direction == 1: # direction: left -> right # get number of grids closer to goal num_grids_to_goal = len([i for i in x_grid if i > player_x]) # maximum == number of grids to goal and a Pitch Control value of 1, # meaning that it is totally controlled by attacker max_pitch_control = num_grids_to_goal * n_grid_cells_y # get the pitch control using the pitch control values that are created above (PPCa) pitch_control_att_team = sum(sum(PPCa[-num_grids_to_goal:])) else: # direction: right -> left num_grids_to_goal = len([i for i in x_grid if i < player_x]) max_pitch_control = num_grids_to_goal * n_grid_cells_y pitch_control_att_team = sum(sum(PPCa[:num_grids_to_goal])) percentage_pitch_control = round( (pitch_control_att_team / max_pitch_control) * 100, 2) return percentage_pitch_control
# region Laurie's code game_id = 2 # let's look at sample match 2 # read in the event data events = mio.read_event_data(DATADIR, game_id) # read 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 (note change in Metrica's coordinate system since the last lesson) tracking_home = mio.to_metric_coordinates(tracking_home) tracking_away = mio.to_metric_coordinates(tracking_away) events = mio.to_metric_coordinates(events) # reverse direction of play in the second half so that home team is always attacking from right->left tracking_home, tracking_away, events = mio.to_single_playing_direction( tracking_home, tracking_away, events) # Calculate player velocities tracking_home = mvel.calc_player_velocities(tracking_home, smoothing=True) tracking_away = mvel.calc_player_velocities(tracking_away, smoothing=True) params = mpc.default_model_params(3) # endregion # Get GK numbers GK_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ]
""" **** pitch control for passes leading up to goal 2 **** """ #Get all shots and goals in the match shots = events[events['Type'] == 'SHOT'] goals = shots[shots['Subtype'].str.contains('-GOAL')].copy() print(goals) #Plot the 3 events leading up to a goal mviz.plot_events(events.loc[820:823], color='k', indicators=['Marker', 'Arrow'], annotate=True) #First get pitch control model parameters params = mpc.default_model_params() #Find goalkeepers for offside calculation GK_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ] #Evaluated pitch control surface for first pass PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event(820, events, tracking_home, tracking_away, params, GK_numbers, field_dimen=( 106.,
filter_='mooving average') # **** NOTE ***** # if the lines above produce an error (happens for one version of numpy) change them to the lines below: # *************** #tracking_home = mvel.calc_player_velocities(tracking_home,smoothing=True,filter_='moving_average') #tracking_away = mvel.calc_player_velocities(tracking_away,smoothing=True,filter_='moving_average') """ **** pitch control for passes leading up to goal 2 **** """ # plot the 3 events leading up to the second goal mviz.plot_events(events.loc[0:3], color='k', indicators=['Marker', 'Arrow'], annotate=False) # first get model parameters params = mpc.default_model_params(3) # evaluated pitch control surface for first pass PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event(0, events, tracking_home, tracking_away, params, field_dimen=( 106., 68., ), n_grid_cells_x=50) mviz.plot_pitchcontrol_for_event(0, events, tracking_home,
def player_pitch_control_impact(self): st.subheader("Simulation") # read dataset df_dict, color_dict, events_df = read_dataset(self.base_dir, self.play, self.args) # show dataframe st.markdown("event dataframe is ...") st.table(events_df) event_id = st.selectbox("Select a event id for analysis", events_df.index, index=events_df.index[-1]) team = st.selectbox( "Select a team for analysis", [events_df.at[event_id, "Team"]] + [ k for k in list(df_dict.keys()) if k != events_df.at[event_id, "Team"] ], ) # sort player num based on end location end_frame = events_df.at[event_id, "End Frame"] ball_loc = events_df.loc[event_id, ["End X", "End Y"]].values player_num_list = list( set([ c.split("_")[1] for c in df_dict[team].columns if c.startswith(team) ])) sorted_index = np.argsort([ np.linalg.norm(df_dict[team].loc[ end_frame, [f"{team}_{player_num}_{c}" for c in ["x", "y"]]].values - ball_loc) for player_num in player_num_list ]) player_num = st.selectbox( "Select a player number for analysis", np.array(player_num_list)[sorted_index], ) verification_mode = st.selectbox( "Select the verification mode", [ "movement:How much space created during event??", "presense:How much space occupied during event??", "location:if positions changed, how much space difference during event??", ], ).split(":")[0] params = mpc.default_model_params(3) example_player_analysis_away = PlayerPitchControlAnalysisPlayer( df_dict=df_dict, params=params, events=events_df, event_id=event_id, team_player_to_analyze=team, player_to_analyze=str(player_num), field_dimens=(106.0, 68.0), n_grid_cells_x=50, ) with st.spinner("wait for computing ..."): if verification_mode == "movement": st.markdown( example_player_analysis_away.team_player_to_analyze + " Player " + str(example_player_analysis_away.player_to_analyze) + " created " + str( int( example_player_analysis_away. calculate_space_created( replace_function="movement", replace_x_velocity=0, replace_y_velocity=0, ))) + " m^2 of space with his movement during event " + str(example_player_analysis_away.event_id)) # Now, let's plot the space created and conceded by his run fig, ax = example_player_analysis_away.plot_pitch_control_difference( replace_function="movement", replace_x_velocity=0, replace_y_velocity=0, team_color_dict=color_dict, ) st.pyplot(fig, bbox_layout="tight") elif verification_mode == "presense": st.markdown( example_player_analysis_away.team_player_to_analyze + " Player " + str(example_player_analysis_away.player_to_analyze) + " occupied " + str( int( example_player_analysis_away. calculate_space_created( replace_function="presence"))) + " m^2 of space during event " + str(example_player_analysis_away.event_id)) fig, ax = example_player_analysis_away.plot_pitch_control_difference( replace_function="presence", team_color_dict=color_dict) st.pyplot(fig, bbox_layout="tight") elif verification_mode == "location": st_frame = events_df.at[event_id, "Start Frame"] x, y = ( df_dict[team].at[st_frame, f"{team}_{player_num}_x"], df_dict[team].at[st_frame, f"{team}_{player_num}_y"], ) max_x, min_x = int(x_size / 2 - x), -int(x_size / 2 + x) max_y, min_y = int(y_size / 2 - y), -int(y_size / 2 + y) relative_x = st.slider("relative x", min_value=min_x, max_value=max_x, value=0, step=1) relative_y = st.slider("relative y", min_value=min_y, max_value=max_y, value=0, step=1) st.markdown( example_player_analysis_away.team_player_to_analyze + " Player " + str(example_player_analysis_away.player_to_analyze) + " would have occupied a difference of " + str( int(-1 * example_player_analysis_away. calculate_space_created( replace_function="location", relative_x_change=relative_x, relative_y_change=relative_y, ))) + " m^2 of space during event " + str(example_player_analysis_away.event_id) + " if they were changed to x, y = " + str(relative_x) + ", " + str(relative_y)) fig, ax = example_player_analysis_away.plot_pitch_control_difference( replace_function="location", relative_x_change=relative_x, relative_y_change=relative_y, team_color_dict=color_dict, ) st.pyplot(fig, bbox_layout="tight") st.success("done !!")
def __init__( self, df_dict, params, events, event_id, team_player_to_analyze, player_to_analyze, field_dimens=(106.0, 68.0), n_grid_cells_x=50, ): """ This class is used to consolidate many of the functions that would be used to analyze the impact of an individual on the pitch control surface during a given event of a match Leveraging @EightyFivePoint's pitch control model presented in the Friends of Tracking Series, we build out a series of tools to help isolate individual player's impacts to pitch control. Using an event from the match's event DataFrame, and a specific team/player ID, this class allows us to: 1. Calculate the amount of space occupied on the pitch (per EightyFivePoint's pitch control model) for any frame of a match. 2. Calculate the difference in total space occupied by the player's team and plot the difference in pitch control surfaces with his/her current movement relative to a theoretical velocity vector (which defaults to no movement). 3. Calculate the difference in total space occupied by the player's team and plot the difference in pitch control surfaces relative to if the player were not on the pitch at all. 4. Calculate the difference in total space occupied by the player's team and plot the difference in pitch control surfaces relative to if the player were in a different location on the pitch and containing a new velocity vector In the relevant functions for plotting pitch control difference and space creation, the ``replace_function`` argument determines what type of analysis we wish to carry out. If replace_function=``movement``, we study the player's impact on pitch control relative to the pitch control if the player had a different velocity vector. velocity vector. If replace_function=``presence``, we study the player's total impact on pitch control by comparing the pitch control surface to the pitch control surface if the player were not on the pitch at all. If replace_function=``location``, we study the player's impact on pitch control relative to the pitch control if the player were in a different location on the pitch. Examples of using this class for each type of analysis are contained in the file ``player_analysis_example.py``. Modifications to @EightyFivePoint's code for plotting pitch control to support our new plots can be found in ``Metrica_viz.py`` Initialization parameters: :param dict df_dict : keys=team_list, values=pd.DataFrame witb velocity for each player :param dict params: Dictionary of model parameters (default model parameters can be generated using default_model_params()) :param pd.DataFrame events: DataFrame containing the event data :param int event_id: Index (not row) of the event that describes the instant at which the pitch control surface should be calculated :param str team_player_to_analyze: The team of the player whose movement we want to analyze. Must be either "Home" or "Away" :param int or str(int) player_to_analyze: The player ID of the player whose movement we want to analyze. The ID must be a player currently on the pitch for ``team_player_to_analyze`` :param tuple field_dimens: tuple containing the length and width of the pitch in meters. Default is (106,68) :param int n_grid_cells_x: Number of pixels in the grid (in the x-direction) that covers the surface. Default is 50. n_grid_cells_y will be calculated based on n_grid_cells_x and the field dimensions """ self.df_dict = df_dict self.params = params self.events = events self.event_id = event_id self.team_player_to_analyze = team_player_to_analyze self.player_to_analyze = player_to_analyze self.field_dimens = field_dimens self.n_grid_cells_x = n_grid_cells_x ( self.event_pitch_control, self.xgrid, self.ygrid, ) = mpc.generate_pitch_control_for_event( event_id=self.event_id, events=self.events, df_dict=self.df_dict, params=self.params, )
def lastrow_generate_pitch_control_for_event(play, event_frame, events, tracking_home, tracking_away, params, field_dimen=( 106., 68., ), n_grid_cells_x=50): import Metrica_PitchControl as mpc """ generate_pitch_control_for_event Evaluates pitch control surface over the entire field at the moment of the given event (determined by the index of the event passed as an input) 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: tracking DataFrame for the Home team tracking_away: tracking DataFrame for the Away team params: Dictionary of model parameters (default model parameters can be generated using default_model_params() ) field_dimen: tuple containing the length and width of the pitch in meters. Default is (106,68) n_grid_cells_x: Number of pixels in the grid (in the x-direction) that covers the surface. Default is 50. n_grid_cells_y will be calculated based on n_grid_cells_x and the field dimensions Returrns ----------- PPCFa: Pitch control surface (dimen (n_grid_cells_x,n_grid_cells_y) ) containing pitch control probability for the attcking team. Surface for the defending team is just 1-PPCFa. xgrid: Positions of the pixels in the x-direction (field length) ygrid: Positions of the pixels in the y-direction (field width) """ # get the details of the event (frame, team in possession, ball_start_position) #play = event_id[0] #event_frame = event_id[1] event_frame = int(event_frame) tracking_frame = events.loc[(play, int(event_frame))]['Start Frame'] ball_start_pos = np.array([ events.loc[(play, event_frame)]['Start X'], events.loc[(play, event_frame)]['Start Y'] ]) # break the pitch down into a grid n_grid_cells_y = int(n_grid_cells_x * field_dimen[1] / field_dimen[0]) xgrid = np.linspace(-field_dimen[0] / 2., field_dimen[0] / 2., n_grid_cells_x) ygrid = np.linspace(-field_dimen[1] / 2., field_dimen[1] / 2., n_grid_cells_y) # initialise pitch control grids for attacking and defending teams PPCFa = np.zeros(shape=(len(ygrid), len(xgrid))) PPCFd = np.zeros(shape=(len(ygrid), len(xgrid))) # initialise player positions and velocities for pitch control calc (so that we're not repeating this at each grid cell position) attacking_players = lastrow_initialise_players( tracking_home.loc[(play, tracking_frame)], 'attack', params) defending_players = lastrow_initialise_players( tracking_away.loc[(play, tracking_frame)], 'defense', params) # calculate pitch pitch control model at each location on the pitch for i in range(len(ygrid)): for j in range(len(xgrid)): target_position = np.array([xgrid[j], ygrid[i]]) PPCFa[i, j], PPCFd[i, j] = mpc.calculate_pitch_control_at_target( target_position, attacking_players, defending_players, ball_start_pos, params) # check probabilitiy sums within convergence checksum = np.sum(PPCFa + PPCFd) / float(n_grid_cells_y * n_grid_cells_x) assert 1 - checksum < params[ 'model_converge_tol'], "Checksum failed: %1.3f" % (1 - checksum) return PPCFa, xgrid, ygrid
def save_match_clip_pcf(hometeam, awayteam, fpath, params, fname='clip_test', figax=None, frames_per_second=25, team_colors=('r', 'b'), field_dimen=(106.0, 68.0), include_player_velocities=False, PlayerMarkerSize=10, PlayerAlpha=0.7, n_grid_cells_x=50): """ save_match_clip( hometeam, awayteam, fpath ) Generates a movie from Metrica tracking data, saving it in the 'fpath' directory with name 'fname' Parameters ----------- hometeam: home team tracking data DataFrame. Movie will be created from all rows in the DataFrame awayteam: away team tracking data DataFrame. The indices *must* match those of the hometeam DataFrame fpath: directory to save the movie fname: movie filename. Default is 'clip_test.mp4' fig,ax: Can be used to pass in the (fig,ax) objects of a previously generated pitch. Set to (fig,ax) to use an existing figure, or None (the default) to generate a new pitch plot, frames_per_second: frames per second to assume when generating the movie. Default is 25. team_colors: Tuple containing the team colors of the home & away team. Default is 'r' (red, home team) and 'b' (blue away team) field_dimen: tuple containing the length and width of the pitch in meters. Default is (106,68) include_player_velocities: Boolean variable that determines whether player velocities are also plotted (as quivers). Default is False PlayerMarkerSize: size of the individual player marlers. Default is 10 PlayerAlpha: alpha (transparency) of player markers. Defaault is 0.7 Returrns ----------- fig,ax : figure and aixs objects (so that other data can be plotted onto the pitch) """ # check that indices match first assert np.all(hometeam.index == awayteam.index ), "Home and away team Dataframe indices must be the same" # in which case use home team index index = hometeam.index # Set figure and movie settings FFMpegWriter = animation.writers['ffmpeg'] metadata = dict(title='Tracking Data', artist='Matplotlib', comment='Metrica tracking data clip') writer = FFMpegWriter(fps=frames_per_second, metadata=metadata) fname = fpath + '/' + fname + '.mp4' # path and filename # create football pitch if figax is None: fig, ax = plot_pitch(field_color='white', field_dimen=field_dimen) else: fig, ax = figax fig.set_tight_layout(True) # Generate movie print("Generating movie...", end='') with writer.saving(fig, fname, 100): for i in index: print(i) figobjs = [] for team, color in zip([hometeam.loc[i], awayteam.loc[i]], team_colors): x_columns = [ c for c in team.keys() if c[-2:].lower() == '_x' and c != 'ball_x' ] # column header for player x positions y_columns = [ c for c in team.keys() if c[-2:].lower() == '_y' and c != 'ball_y' ] # column header for player y positions objs, = ax.plot(team[x_columns], team[y_columns], color + 'o', MarkerSize=PlayerMarkerSize, alpha=PlayerAlpha) # plot player positions figobjs.append(objs) if include_player_velocities: vx_columns = ['{}_vx'.format(c[:-2]) for c in x_columns ] # column header for player x positions vy_columns = ['{}_vy'.format(c[:-2]) for c in y_columns ] # column header for player y positions objs = ax.quiver(team[x_columns], team[y_columns], team[vx_columns], team[vy_columns], color=color, scale_units='inches', scale=10., width=0.0015, headlength=5, headwidth=3, alpha=PlayerAlpha) figobjs.append(objs) # plot ball n_grid_cells_y = int(n_grid_cells_x * field_dimen[1] / field_dimen[0]) xgrid = np.linspace(-field_dimen[0] / 2., field_dimen[0] / 2., n_grid_cells_x) ygrid = np.linspace(-field_dimen[1] / 2., field_dimen[1] / 2., n_grid_cells_y) # initialise pitch control grids for attacking and defending teams PPCFa = np.zeros(shape=(len(ygrid), len(xgrid))) PPCFd = np.zeros(shape=(len(ygrid), len(xgrid))) ball_start_pos = np.array( [hometeam.loc[i]['ball_x'], hometeam.loc[i]['ball_y']]) attacking_players = mpc.initialise_players(hometeam.loc[i], 'Home', params) defending_players = mpc.initialise_players(awayteam.loc[i], 'Away', params) for k in range(len(ygrid)): for j in range(len(xgrid)): target_position = np.array([xgrid[j], ygrid[k]]) PPCFa[k, j], PPCFd[k, j] = mpc.calculate_pitch_control_at_target( target_position, attacking_players, defending_players, ball_start_pos, params) # check probabilitiy sums within convergence checksum = np.sum(PPCFa + PPCFd) / float( n_grid_cells_y * n_grid_cells_x) assert 1 - checksum < params[ 'model_converge_tol'], "Checksum failed: %1.3f" % (1 - checksum) pcf = ax.imshow(np.flipud(PPCFa), extent=(np.amin(xgrid), np.amax(xgrid), np.amin(ygrid), np.amax(ygrid)), interpolation='hanning', vmin=0.0, vmax=1.0, cmap='bwr', alpha=0.5) figobjs.append(pcf) objs, = ax.plot(team['ball_x'], team['ball_y'], 'ko', MarkerSize=6, alpha=1.0, LineWidth=0) figobjs.append(objs) # include match time at the top frame_minute = int(team['Time [s]'] / 60.) frame_second = (team['Time [s]'] / 60. - frame_minute) * 60. timestring = "%d:%1.2f" % (frame_minute, frame_second) objs = ax.text(-2.5, field_dimen[1] / 2. + 1., timestring, fontsize=14) figobjs.append(objs) writer.grab_frame() # Delete all axis objects (other than pitch lines) in preperation for next frame for figobj in figobjs: figobj.remove() print("done") plt.clf() plt.close(fig)
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.0, 68.0, ), 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 calculate_pitch_control_new_location( self, relative_x_change, relative_y_change, replace_velocity=False, replace_x_velocity=0, replace_y_velocity=0, ): """ Function description: This function calculates a pitch control surface after moving the player to a new location on the pitch, and specifying a new velocity vector for the player. Ideally, this function would be used to identify/illustrate where players could be located/moving towards in order to maximize their team's pitch control Input parameters: :param float relative_x_change: The amount to change the x coordinate of the player by before calculating the new pitch control model. Measured in meters :param float relative_y_change: The amount to change the y coordinate of the player by before calculating the new pitch control model. Measured in meters. :param bool replace_velocity: Tells us whether to replace the player's velocity vector with a new one. If False, the player's velocity vector will remain the same. If True, the player's velocity will be placed with the values in the ``replace_x_velocity`` and replace_y_velocity`` argument. Default is False. :param float replace_x_velocity: The x vector of the velocity we would like to replace our given player with. Positive values will move the player toward's the home team's goal, while negative values will move the player towards the Away Team's goal. Measured in m/s. Defaults to 0. :param float replace_y_velocity: The y vector of the velocity we would like to replace our given player with. Positive values will move the player to the left side of the pitch, from the perspective of the away team, while negative values will move the player towards the right side of the pitch from the perspective of the away team. Measured in m/s. Defaults to 0. Returns: edited_pitch_control: Pitch control surface (dimen (n_grid_cells_x,n_grid_cells_y) ) containing pitch control probability for the attcking team with one player's velocity changed Surface for the defending team is just 1-PPCFa. xgrid: Positions of the pixels in the x-direction (field length) ygrid: Positions of the pixels in the y-direction (field width) """ self._validate_inputs() if replace_velocity & (replace_x_velocity == 0) & (replace_y_velocity == 0): warnings.warn( "You have not specified a new velocity vector for the player. All analysis will assume " "that the player is stationary in his/her new location") event_frame = self.events.loc[self.event_id]["Start Frame"] # Replace datapoints with a new location and velocity vector tmp_df_dict = self.df_dict.copy() for team, df in tmp_df_dict.items(): if team == self.team_player_to_analyze: df_tmp = df.copy() df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_x", ] = relative_x_change df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_y", ] = relative_y_change df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vx", ] = replace_x_velocity df_tmp.at[ event_frame, f"{self.team_player_to_analyze}_{self.player_to_analyze}_vy", ] = replace_y_velocity tmp_df_dict[team] = df_tmp edited_pitch_control, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id=self.event_id, events=self.events, df_dict=tmp_df_dict, params=self.params, field_dimen=self.field_dimens, n_grid_cells_x=self.n_grid_cells_x, ) return edited_pitch_control, xgrid, ygrid
ascending=False) # We want the top 5 not including the goalie (Player 11) top_passers = players_averages[(players_averages['Overplayed per pass'] >= 1.8) & (players_averages.index != 'Player11')] #We can plot the passes from each player in the top 5. Only passes that broke through 3 away opponents. for i in top_passers.index: pass_success_probability = [] player_passing = index[(index.From == i) & (index.Break >= 3)] for i, row in player_passing.iterrows(): pass_start_pos = np.array([row['Start X'], row['Start Y']]) pass_target_pos = np.array([row['End X'], row['End Y']]) pass_frame = row['Start Frame'] attacking_players = mpc.initialise_players( tracking_home.loc[pass_frame], 'Home', params) defending_players = mpc.initialise_players( tracking_away.loc[pass_frame], 'Away', params) Patt, _ = mpc.calculate_pitch_control_at_target( pass_target_pos, attacking_players, defending_players, pass_start_pos, params) pass_success_probability.append((i, Patt)) fig, ax = plt.subplots() ax.hist([p[1] for p in pass_success_probability], bins=8) ax.set_xlabel('Pass success probability') ax.set_ylabel('Frequency') pass_success_probability = sorted(pass_success_probability,
# reverse direction of play in the second half so that home team is always attacking from right->left tracking_home, tracking_away, events = mio.to_single_playing_direction( tracking_home, tracking_away, events) # Calculate player velocities tracking_home = mvel.calc_player_velocities(tracking_home, smoothing=True) tracking_away = mvel.calc_player_velocities(tracking_away, smoothing=True) # **** NOTE ***** # if the lines above produce an error (happens for one version of numpy) change them to the lines below: # *************** #tracking_home = mvel.calc_player_velocities(tracking_home,smoothing=True,filter_='moving_average') #tracking_away = mvel.calc_player_velocities(tracking_away,smoothing=True,filter_='moving_average') """ *** UPDATES TO THE MODEL: OFFSIDES """ # first get pitch control model parameters params = mpc.default_model_params() # find goalkeepers for offside calculation GK_numbers = [ mio.find_goalkeeper(tracking_home), mio.find_goalkeeper(tracking_away) ] """ *** GET EPV SURFACE **** """ home_attack_direction = mio.find_playing_direction( tracking_home, 'Home') # 1 if shooting left-right, else -1 EPV = mepv.load_EPV_grid(DATADIR + '/EPV_grid.csv') # plot the EPV surface mviz.plot_EPV(EPV, field_dimen=(106.0, 68), attack_direction=home_attack_direction) # plot event leading up to first away team goal