Example #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
Example #2
0
#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,
                                      key=lambda x: x[1])

    risky_passes = index.loc[[
        p[0] for p in pass_success_probability if p[1] < 0.5
    ]]
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
Example #4
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)
Example #5
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