コード例 #1
0
    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
コード例 #2
0
    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
コード例 #3
0
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
コード例 #4
0
EPV_start = mepv.get_EPV_at_location(pass_start_pos,
                                     EPV,
                                     attack_direction=attack_direction)

#%%
EPV_target = mepv.get_EPV_at_location(pass_target_pos,
                                      EPV,
                                      attack_direction=attack_direction)
EPV_start, EPV_target
#%%
PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event(event_number,
                                                          events,
                                                          tracking_home,
                                                          tracking_away,
                                                          params,
                                                          GK_numbers,
                                                          field_dimen=(
                                                              106.,
                                                              68.,
                                                          ),
                                                          n_grid_cells_x=50,
                                                          offsides=True)
#%%
PPCF
# PPCF[0:4,]
xgrid
ygrid
#%%
fig, ax = mviz.plot_EPV_for_event(event_number,
                                  events,
                                  tracking_home,
                                  tracking_away,
# ***************
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,
                                 tracking_away,
                                 PPCF_actual,
                                 xgrid,
                                 ygrid,
                                 annotate=True)

# removing player 23 blue (away) team velocity
コード例 #6
0
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
コード例 #7
0
#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.,
                                                              68.,
                                                          ),
                                                          n_grid_cells_x=50)
mviz.plot_pitchcontrol_for_event(820,
                                 events,
                                 tracking_home,
                                 tracking_away,
                                 PPCF,
                                 annotate=True)
#Evaluated pitch control surface for second pass
PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event(821,
                                                          events,
                                                          tracking_home,
コード例 #8
0
    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
コード例 #9
0
    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,
        )