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
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,
def find_max_value_added_target(event_id, events, tracking_home, tracking_away, GK_numbers, EPV, params): """ find_max_value_added_target Finds the *maximum* expected possession value that could have been achieved for a pass (defined by the event_id) by searching the entire field for the best target. Parameters ----------- event_id: Index (not row) of the pass event to calculate EPV-added score events: Dataframe containing the event data tracking_home: tracking DataFrame for the Home team tracking_away: tracking DataFrame for the Away team GK_numbers: tuple containing the player id of the goalkeepers for the (home team, away team) EPV: tuple Expected Possession value grid (loaded using load_EPV_grid() ) params: Dictionary of pitch control model parameters (default model parameters can be generated using default_model_params() ) Returrns ----------- maxEPV_added: maximum EPV value-added that could be achieved at the current instant max_target_location: (x,y) location of the position of the maxEPV_added """ # pull out pass details from the event data pass_start_pos = np.array( [events.loc[event_id]["Start X"], events.loc[event_id]["Start Y"]]) pass_frame = events.loc[event_id]["Start Frame"] pass_team = events.loc[event_id].Team # direction of play for atacking team (so we know whether to flip the EPV grid) home_attack_direction = mio.find_playing_direction(tracking_home, "Home") if pass_team == "Home": attack_direction = home_attack_direction attacking_players = mpc.initialise_players( tracking_home.loc[pass_frame], "Home", params, GK_numbers[0]) defending_players = mpc.initialise_players( tracking_away.loc[pass_frame], "Away", params, GK_numbers[1]) elif pass_team == "Away": attack_direction = home_attack_direction * -1 defending_players = mpc.initialise_players( tracking_home.loc[pass_frame], "Home", params, GK_numbers[0]) attacking_players = mpc.initialise_players( tracking_away.loc[pass_frame], "Away", params, GK_numbers[1]) # flag any players that are offside attacking_players = mpc.check_offsides(attacking_players, defending_players, pass_start_pos, GK_numbers) # pitch control grid at pass start location Patt_start, _ = mpc.calculate_pitch_control_at_target( pass_start_pos, attacking_players, defending_players, pass_start_pos, params) # EPV at start location EPV_start = get_EPV_at_location(pass_start_pos, EPV, attack_direction=attack_direction) # calculate pitch control surface at moment of the pass PPCF, xgrid, ygrid = mpc.generate_pitch_control_for_event( event_id, events, tracking_home, tracking_away, params, GK_numbers, field_dimen=( 106.0, 68.0, ), n_grid_cells_x=50, offsides=True, ) # EPV surface at instance of the pass if attack_direction == -1: EEPV = np.fliplr(EPV) * PPCF else: EEPV = EPV * PPCF # find indices of the maxEPV maxEPV_idx = np.unravel_index(EEPV.argmax(), EEPV.shape) # Expected EPV at current ball position EEPV_start = Patt_start * EPV_start # maxEPV_added (difference between max location and current ball location) maxEPV_added = EEPV.max() - EEPV_start # location of maximum max_target_location = (xgrid[maxEPV_idx[1]], ygrid[maxEPV_idx[0]]) return maxEPV_added, max_target_location
def 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)
#%% event_number = 822 # 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) #%%