Beispiel #1
0
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
Beispiel #2
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
    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
Beispiel #4
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
Beispiel #5
0
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
Beispiel #6
0
# 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)
]
Beispiel #10
0
""" **** 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.,
Beispiel #11
0
                                            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,
Beispiel #12
0
    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 !!")
Beispiel #13
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,
        )
Beispiel #14
0
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
Beispiel #15
0
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
Beispiel #17
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
Beispiel #18
0
                                                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