def calc_posession_lattice(posessions, frames_tb, match_tb, subsample): # parity=1, home team is attacking right->left frames = [] fig,ax = vis.plot_pitch(match_tb) fig1,ax1 = vis.plot_pitch(match_tb) # note all posessions **must** be the same team team = posessions[0].team for pos in posessions: assert pos.team==team frames = frames + frames_tb[pos.pos_start_fnum:pos.pos_end_fnum+1] form_Att = formation(team) form_Def = formation(team) for frame in frames[::subsample]: if posessions[0].team=='H': attacking_players = frame.team1_players defending_players = frame.team0_players parity = match_tb.period_parity[frame.period] else: attacking_players = frame.team0_players defending_players = frame.team1_players parity = match_tb.period_parity[frame.period]*-1 form_Att.add_latice( lattice('A',parity,frame.timestamp,attacking_players,[1,31]) ) form_Def.add_latice( lattice('D',parity*-1,frame.period,frame.timestamp,defending_players,[1,31]) ) print (form_Att.pids) print (form_Def.pids form_Def.calc_average_lattice)(match_tb) form_Att.calc_average_lattice(match_tb) offsets = calc_offset_between_formations(form_Def,form_Att,calc='offside',parity=parity) form_Def.plot_formation(factor=100,figax=(fig,ax),lt='bo') form_Att.plot_formation(factor=100,offsets=offsets,figax=(fig1,ax1),lt='ro') return form_Def,form_Att
def plot_defensive_actions(team,all_matches,include_tackles=True,include_intercept=True): match_OPTA = all_matches[0] # tackles won and retained posession team_events = [] for match in all_matches: if match.hometeam.teamname==team: team_events += match.hometeam.events elif match.awayteam.teamname==team: team_events += match.awayteam.events tackle_won = [e for e in team_events if e.type_id == 7 and e.outcome==1 and 167 not in e.qual_id_list] # interceptions intercepts = [e for e in team_events if e.type_id == 8] fig,ax = vis.plot_pitch(all_matches[0]) xfact = match_OPTA.fPitchXSizeMeters*100 yfact = match_OPTA.fPitchYSizeMeters*100 count = 0 # tackles if include_tackles: for tackle in tackle_won: ax.plot(tackle.x*xfact,tackle.y*yfact,'rd',alpha=0.6,markersize=10,label=count) count +=1 if include_intercept: for intercept in intercepts: ax.plot(intercept.x*xfact,intercept.y*yfact,'rs',alpha=0.6,markersize=10,label=count) count +=1
def plot_all_passes(match_OPTA): fig, ax = vis.plot_pitch(match_OPTA) home_passes = [e for e in match_OPTA.hometeam.events if e.is_pass] away_passes = [e for e in match_OPTA.awayteam.events if e.is_pass] xfact = match_OPTA.fPitchXSizeMeters * 100 yfact = match_OPTA.fPitchYSizeMeters * 100 for p in home_passes: ax.arrow(p.pass_start[0] * xfact, p.pass_start[1] * yfact, (p.pass_end[0] - p.pass_start[0]) * xfact, (p.pass_end[1] - p.pass_start[1]) * yfact, color='b', length_includes_head=True, head_width=0.08 * xfact, head_length=0.00002 * yfact) for p in away_passes: ax.arrow(p.pass_start[0] * xfact, p.pass_start[1] * yfact, (p.pass_end[0] - p.pass_start[0]) * xfact, (p.pass_end[1] - p.pass_start[1]) * yfact, color='r', length_includes_head=True, head_width=0.08 * xfact, head_length=0.00002 * yfact) match_string = '%s %d vs %d %s' % ( match_OPTA.hometeam.teamname, match_OPTA.homegoals, match_OPTA.awaygoals, match_OPTA.awayteam.teamname) plt.title(match_string, fontsize=16, y=1.0) plt.waitforbuttonpress(0) return fig, ax
def Hungarian_Cost(F1, F2, match_tb, metric='L2', squeeze=[1.], plot=False): # calculates cost of player assignments between any two roles (L1 or L2 norm) in each formation # this process tells us how to match a player in one formation observation to a player in another to minimize the overal formation comparison cost # uses the Kuhn-Munkres algorithm pids1 = np.array(F1.pids) pids2 = np.array(F2.pids) n1 = len(pids1) n2 = len(pids2) if len(squeeze) > 1: # marginalise over the squeeze paramter cost = [] for s in squeeze: C = np.zeros(shape=(n1, n2)) for i in range(n1): pid1 = pids1[i] for j in np.arange(n2): pid2 = pids2[j] C[i, j] = Cost_Metric(F1, F2, pid1, pid2, metric=metric, s=s) row_ind, col_ind = linear_sum_assignment(C) cost.append((s, C[row_ind, col_ind].sum(), col_ind)) cost = sorted(cost, key=lambda x: x[1]) col_ind_min = cost[0][2] cost_min = cost[0][1] squeeze_min = cost[0][0] # print squeeze_min pmatch = pids2[col_ind_min] else: # just use a single squeeze parameter. Obviously can simplify this. squeeze_min = squeeze[0] C = np.zeros(shape=(n1, n2)) for i in range(n1): pid1 = pids1[i] for j in range(n2): pid2 = pids2[j] C[i, j] = Cost_Metric(F1, F2, pid1, pid2, metric=metric, s=squeeze_min) row_ind, col_ind = linear_sum_assignment(C) cost_min = C[row_ind, col_ind].sum() pmatch = pids2[col_ind] if plot: fig, ax = vis.plot_pitch(match_tb) F1.plot_formation(factor=100, figax=(fig, ax), lt='ro') F2.plot_formation(factor=100, figax=(fig, ax), lt='bo') for i in range(n1): pid1 = pids1[i] pid2 = pmatch[i] ax.plot( [100 * F1.nodes[pid1].x, squeeze_min * 100 * F2.nodes[pid2].x], [100 * F1.nodes[pid1].y, squeeze_min * 100 * F2.nodes[pid2].y], 'k') return cost_min, pids1, pmatch, squeeze_min
def plot_all_passes(events,match_tb,window=(0,200),figax = None, flip=1.0,color='blue',alpha=0.3): if figax is None: fig,ax = vis.plot_pitch(match_tb) else: fig,ax = figax xfact = match_tb.fPitchXSizeMeters*100*flip yfact = match_tb.fPitchYSizeMeters*100*flip all_pts = [] for e in events: if e.type_id in [1,2] and e.time>=window[0] and e.time<window[1] and 6 not in e.qual_id_list: # pass event pass_end_x = e.qualifiers[e.qual_id_list.index(140)].value/100. - 0.5 pass_end_y = e.qualifiers[e.qual_id_list.index(141)].value/100. - 0.5 x = np.array([e.x,pass_end_x])*xfact y = np.array([e.y,pass_end_y])*yfact #ax.plot(x,y,'r',alpha=0.6) p = ax.annotate('', xy=(x[1], y[1]), xytext=(x[0], y[0]),arrowprops=dict(arrowstyle="-|>",linewidth=1,color=color, alpha=alpha) ) all_pts.append(p) return fig,ax,all_pts
def plot_all_shots(match_OPTA,plotly=False,twindow=(0,200),figax=None,pt='o'): symbols = lambda x: 'd' if x=='Goal' else pt if figax is None: fig,ax = vis.plot_pitch(match_OPTA) else: fig,ax = figax homeshots = [e for e in match_OPTA.hometeam.events if e.is_shot and e.time>=twindow[0] and e.time<twindow[1]] awayshots = [e for e in match_OPTA.awayteam.events if e.is_shot and e.time>=twindow[0] and e.time<twindow[1]] xfact = match_OPTA.fPitchXSizeMeters*100 yfact = match_OPTA.fPitchYSizeMeters*100 descriptors = {} count = 0 for shot in homeshots: descriptors[str(count)] = shot.shot_descriptor ax.plot(shot.x*xfact,shot.y*yfact,'r'+symbols(shot.description),alpha=0.6,markersize=20*np.sqrt(shot.expG_caley),label=count) count += 1 for shot in awayshots: descriptors[str(count)] = shot.shot_descriptor ax.plot(-1*shot.x*xfact,-1*shot.y*yfact,'b'+symbols(shot.description),alpha=0.6,markersize=20*np.sqrt(shot.expG_caley),label=count) count += 1 home_xG = np.sum([s.expG_caley2 for s in homeshots]) away_xG = np.sum([s.expG_caley2 for s in awayshots]) match_string = '%s %d (%1.1f) vs (%1.1f) %d %s' % (match_OPTA.hometeam.teamname,match_OPTA.homegoals,home_xG,away_xG,match_OPTA.awaygoals,match_OPTA.awayteam.teamname) #ax.text(-75*len(match_string),match_OPTA.fPitchYSizeMeters*50+500,match_string,fontsize=20) if plotly: plt.title( match_string, fontsize=20, y=0.95 ) plotly_fig = tls.mpl_to_plotly( fig ) for d in plotly_fig.data: if d['name'] in descriptors.keys(): d['text'] = descriptors[d['name']] d['hoverinfo'] = 'text' else: d['name'] = "" d['hoverinfo'] = 'name' plotly_fig['layout']['titlefont'].update({'color':'black', 'size':20, 'family':'monospace'}) plotly_fig['layout']['xaxis'].update({'ticks':'','showticklabels':False}) plotly_fig['layout']['yaxis'].update({'ticks':'','showticklabels':False}) #url = py.plot(plotly_fig, filename = 'Aalborg-match-analysis') return plotly_fig else: plt.title( match_string, fontsize=16, y=1.0 ) return fig,ax
def plot_defensive_actions(team, all_matches, include_tackles=True, include_intercept=True): match_OPTA = all_matches[0] # tackles won and retained posession team_events = [] for match in all_matches: if match.hometeam.teamname == team: team_events += match.hometeam.events elif match.awayteam.teamname == team: team_events += match.awayteam.events tackle_won = [ e for e in team_events if e.type_id == 7 and e.outcome == 1 and 167 not in e.qual_id_list ] # interceptions intercepts = [e for e in team_events if e.type_id == 8] fig, ax = vis.plot_pitch(all_matches[0]) xfact = match_OPTA.fPitchXSizeMeters * 100 yfact = match_OPTA.fPitchYSizeMeters * 100 count = 0 # tackles if include_tackles: for tackle in tackle_won: ax.plot(tackle.x * xfact, tackle.y * yfact, 'rd', alpha=0.6, markersize=10, label=count) count += 1 if include_intercept: for intercept in intercepts: ax.plot(intercept.x * xfact, intercept.y * yfact, 'rs', alpha=0.6, markersize=10, label=count) count += 1 #match_string = '%s %d (%1.1f) vs (%1.1f) %d %s' % (match_OPTA.hometeam.teamname,match_OPTA.homegoals,home_xG,away_xG,match_OPTA.awaygoals,match_OPTA.awayteam.teamname) #plt.title( match_string, fontsize=16, y=1.0 ) plt.waitforbuttonpress(0)
# plot positions and velocities of the ball in each direction fig,axes = plt.subplots(3,1,figsize=(8,8)) fig2,axes2 = plt.subplots(3,1,figsize=(8,8)) for i,l in zip([0,1,2],['x','y','z']): axes[i].plot(timestamps,ball_positions_xyz[:,i],'r') axes2[i].plot(timestamps,ball_velocities_xyz[:,i],'r') axes[i].set_ylabel(l + '-position (m)') axes2[i].set_ylabel(l + '-velocity (m/s)') # add x-axis labels axes[2].set_xlabel('time (mins)') axes2[2].set_xlabel('time (mins)') # EXAMPLE: make a plot of a player 2's position (home team) over the first half # this is a bit clumsy fig,ax = vis.plot_pitch(match_tb) # plot pitch px = np.array( [f.pos_x for f in team1_players[2].frame_targets] ) py = np.array( [f.pos_y for f in team1_players[2].frame_targets] ) t = np.array( team1_players[2].frame_timestamps ) flast = vis.find_framenum_at_timestamp(frames_tb,match_tb,2,0) # first frame of second half ax.plot( px[0:flast],py[0:flast],'r.') # EXAMPLE: make a timeseries plot of player 2's velocity (x and y components) and speed over the first half vx = np.array( [f.vx for f in team1_players[2].frame_targets] ) vy = np.array( [f.vy for f in team1_players[2].frame_targets] ) fig,ax = plt.subplots() ax.plot( t[0:flast], vx[0:flast], 'r' ) ax.plot( t[0:flast], vy[0:flast], 'b' )
def calc_formation_by_period(period_length,min_period_length,posessions,frames_tb,match_tb, subsample,excludeH,excludeA,plotfig=False,deleteLattices=False): # aggregates individual posessions into windoes of at least min_period_length and at most period_length # then calculates attackoing (team in possession) and defensive (team out of possession) formations in each possession window # posessions is a list of possessions # excludeH/excludeA are player ids to exclude (the goalkeepers) # subsample reduces framerate to speed up calculation # note all posessions **must** be the same team team = posessions[0].team # the team that has possession in the first possession of the list (Home or away) # group posessions into chunks period_posessions = [] # each element corresponds to a list of frames for the aggregated possessions count = 0.0 # this is a counter that tracks how much playing time we have in each aggregated possession window frames = [] # set of player ids that appear in the first frame of the match pids_H = set( frames_tb[0].team1_jersey_nums_in_frame ) pids_A = set( frames_tb[0].team0_jersey_nums_in_frame ) for pos in posessions: assert pos.team==team # make sure that we haven't switched team # has there been a substituion? sub = pids_H != set( frames_tb[pos.pos_start_fnum].team1_jersey_nums_in_frame ) or pids_A != set( frames_tb[pos.pos_start_fnum].team0_jersey_nums_in_frame ) if sub: # there was a substituion #print "sub", count, frames_tb[pos.pos_start_fnum].timestamp, frames_tb[pos.pos_end_fnum].timestamp period_posessions.append(frames) # end possession window frames = frames_tb[pos.pos_start_fnum:pos.pos_end_fnum+1] # start new possession window count = pos.pos_duration pids_H = set( frames_tb[pos.pos_start_fnum].team1_jersey_nums_in_frame ) pids_A = set( frames_tb[pos.pos_start_fnum].team0_jersey_nums_in_frame ) else: frames = frames + frames_tb[pos.pos_start_fnum:pos.pos_end_fnum+1] if count >= period_length: # if we have enough time in aggregated possessions, start a new possession window #print count period_posessions.append(frames) frames = [] count = 0 else: count += pos.pos_duration # now we have a list of aggregated possession windows. Remove those that are too short (because of substitutions) period_posessions = [p for p in period_posessions if len(p)>min_period_length*float(match_tb.iFrameRateFps)] attacking_formations = [] defensive_formations = [] # now measure the attacking and defensive formations in each possession window for period_posession in period_posessions: form_Att = formation(team) # 'team' here is technically the team in possession, so the same in both cases form_Def = formation(team) period_start = period_posession[0].period for frame in period_posession[::subsample]: # subsampling to speed this up if team=='H': attacking_players = frame.team1_players defending_players = frame.team0_players parity = match_tb.period_parity[frame.period] # deals with left-right play direction excludeAtt = excludeH # these are the player ids to exclude (goalkeepers) excludeDef = excludeA else: attacking_players = frame.team0_players defending_players = frame.team1_players parity = match_tb.period_parity[frame.period]*-1 excludeAtt = excludeA excludeDef = excludeH form_Att.add_latice( lattice('A',parity,frame.timestamp,attacking_players,excludeAtt ) ) # always make this an attacking 'A' formation, so that the team shoots from right->left form_Def.add_latice( lattice('A',parity*-1,frame.timestamp,defending_players,excludeDef) ) form_Def.calc_average_lattice(match_tb) form_Att.calc_average_lattice(match_tb) offsets = calc_offset_between_formations(form_Def,form_Att,calc='offside',parity=parity) if plotfig: fig,ax = vis.plot_pitch(match_tb) fig1,ax1 = vis.plot_pitch(match_tb) form_Def.plot_formation(factor=100,figax=(fig,ax),lt='bo') form_Att.plot_formation(factor=100,offsets=offsets,figax=(fig1,ax1),lt='ro') if deleteLattices: # reduce memory usage form_Def.delete_lattices() form_Att.delete_lattices() attacking_formations.append(form_Att) defensive_formations.append(form_Def) return attacking_formations,defensive_formations
def ball_movie(match_OPTA, relative_positioning=True, team="home", weighting="regular"): """ Displays estimated player positions and animates ball movement throughout game. Highlights passes and ball losses. TODO: substitutions (for now color ball differently when passed to a sub) Args: match_OPTA (OPTAmatch) relative_positioning (bool): whether the players should be displayed with relative position or average team (string): "home" or "away" weighting (string): type of network positioning to display """ fig, ax = vis.plot_pitch(match_OPTA) team_object = match_OPTA.hometeam if team == "home" else match_OPTA.awayteam events_raw = [ e for e in team_object.events if e.is_pass or e.is_shot or e.is_substitution ] mapped_players = get_player_positions( match_OPTA, relative_positioning=relative_positioning, team=team, weighting=weighting) for player in mapped_players: shrink_factor = 0.25 fig, ax, pt = utils.plot_bivariate_normal([player.x, player.y], player.cov * shrink_factor**2, figax=(fig, ax)) ax.annotate(team_object.player_map[player.id].lastname, (player.x, player.y)) # display ball as black circle being passed # turn ball into red x when lost # turn ball blue when passed to a sub # turn ball into diamond on shot attempt (black for attempt, green for success) last_pass = None ball = None pause_time = 0.1 shot_pause_factor = 10 pass_lines = [] last_location = None for e in events_raw: if ball: ball.remove() ball = None player = team_object.player_map[e.player_id] if last_location: new_pass = ax.arrow(last_location[0], last_location[1], player.x - last_location[0], player.y - last_location[1], length_includes_head=True) pass_lines.append(new_pass) last_location = (player.x, player.y) if e.is_pass: ball = ax.plot(player.x, player.y, color='black', marker='o')[0] plt.pause(pause_time) if e.outcome != 1: ball.remove() ball = ax.plot(player.x, player.y, color='red', marker='X')[0] plt.pause(pause_time) for l in pass_lines: l.remove() pass_lines = [] last_location = None elif e.is_shot: ball = ax.plot(player.x, player.y, color='black', marker='o')[0] plt.pause(pause_time) color = 'black' if e.outcome == 1: color = 'green' ball.remove() ball = ax.plot(player.x, player.y, color=color, marker='D')[0] plt.pause(pause_time * shot_pause_factor) for l in pass_lines: l.remove() pass_lines = [] last_location = None plt.waitforbuttonpress(0)
def plot_passing_network(match_OPTA, weighting="regular", team="home", relative_positioning=True, display_passes=True, wait=True): """ Plot the passing networks of the entire match, displaying player movement as bivariate normal ellipses and arrow weights directly corresponding to the number of passes executed TODO: - account for subs - allow different weighting schema Args: match_OPTA (OPTAmatch): match OPTA information Kwargs: weighting (string): type of pass network. Choices include: regular - all passes included and weighted offensive - weight offensive passes but not defensive defensive - weight defensive passes but not offensive lateral - weight passes with minimal progression in either direction on the field forwards - weight passes in the offensive direction backwards - weight passes in the defensive direction relative_positioning (bool): if True, player positions on the diagram should be relative in terms of formation rather than exact position average """ fig, ax = vis.plot_pitch(match_OPTA) team_object = match_OPTA.hometeam if team == "home" else match_OPTA.awayteam # some passes may be completed and followed by a shot instead of another pass events_raw = [ e for e in team_object.events if e.is_pass or e.is_shot or e.is_substitution ] xfact = match_OPTA.fPitchXSizeMeters * 100 yfact = match_OPTA.fPitchYSizeMeters * 100 mapped_players = get_player_positions( match_OPTA, relative_positioning=relative_positioning, team=team, weighting=weighting) for player in mapped_players: shrink_factor = 0.25 fig, ax, pt = utils.plot_bivariate_normal([player.x, player.y], player.cov * shrink_factor**2, figax=(fig, ax)) ax.plot([player.x, player.y]) ax.annotate( # team_object.player_map[player.id].lastname, player.id, (player.x, player.y)) if display_passes: # Plot network of passes with arrows max_passes = 0 for player in team_object.players: player_max_passes = 0 if player.pass_destinations.values(): player_max_passes = float( sorted(player.pass_destinations.values())[-1]) if player_max_passes > max_passes: max_passes = player_max_passes for player in team_object.players: # if player in exclude_players: if player not in mapped_players: continue for dest_player_id, num_passes in player.pass_destinations.items(): dest_player = team_object.player_map[dest_player_id] if dest_player not in mapped_players: continue # right to left is orange and left to right is red to differentiate directions color = 'red' if dest_player.x > player.x: color = 'orange' max_width = 3 arrow = patches.FancyArrowPatch( (player.x, player.y), (dest_player.x, dest_player.y), connectionstyle="arc3,rad=.1", color=color, arrowstyle= 'Simple,tail_width=0.5,head_width=4,head_length=8', # linewidth=num_passes*0.8, linewidth=max_width * (num_passes / max_passes)) ax.add_artist(arrow) match_string = '%s %d vs %d %s' % ( match_OPTA.hometeam.teamname, match_OPTA.homegoals, match_OPTA.awaygoals, match_OPTA.awayteam.teamname) plt.title(match_string, fontsize=16, y=1.0) if wait: plt.waitforbuttonpress(0) return fig, ax
vis.plot_frame(frame, match, units=1.0, include_player_velocities=False, include_ball_velocities=False) # plot trajectories of attacking players in 3 second window around the corner, starting one second before the corner is taken # first get the frames from 1 second before the corner to 2 seconds after corner_frames = frames[corner_frame_number - 25:corner_frame_number + 2 * 25] # factor of 25 because there are 25 frames/second penalty_area_edge = match.fPitchXSizeMeters / 2. - 16.38 # 16.38 is 18 yards in meters penalty_area_width = 40.04 # 40.04 is 44 yards in meters fig, ax = vis.plot_pitch(match) vis.plot_frame(corner_frames[0], match, units=1.0, include_player_velocities=False, include_ball_velocities=False, figax=(fig, ax)) for cf in corner_frames: if corners.iloc[0]['Team'] == hometeam: players = cf.team1_players # home team players pcolor = 'r.' # for plots direction_of_play = match.period_parity[ frame. period] * -1 # 1 means home team is playing right->left, -1 means left-right (but switch sign for this example) else: