Rocket League Match Score Prediction

The Tabular Playground Series of this month integrates an interesting dataset, a dataset of Rocket League gameplay. This month's challenge is to, given a snapshot from a Rocket League match, predict the probability of each team scoring within the next 10 seconds of the game.

Dataset Description

¶

The dataset consists of sequences of snapshots of the state of a Rocket League match, including position and velocity of all players and the ball, as well as extra information. The data was taken from professional Rocket League matches. Each event consists of a chronological series of frames recorded at 10 frames per second. All events begin with a kickoff, and most end in one team scoring a goal, but some are truncated and end with no goal scored due to circumstances which can cause gameplay strategies to shift.

Files:

  • train_[0-9].csv: Train set split into 10 files. Rows are sorted by game_num, event_id, and event_time, and each event is entirely contained in one file.
  • test.csv: Test set. Unlike the train set, the rows are scrambled.

Columns Description from train/test files:

  • game_num (train only): Unique identifier for the game from which the event was taken.
  • event_id (train only): Unique identifier for the sequence of consecutive frames.
  • event_time (train only): Time in seconds before the event ended, either by a goal being scored or simply when we decided to truncate the timeseries if a goal was not scored.

Position and Velocity data from ball and players:

  • ballpos[xyz]: Ball's position as a 3d vector.
  • ballvel[xyz]: Ball's velocity as a 3d vector.

Additional Data:

For i in [0,5]:

  • p{i}pos[xyz]: Player i's position as a 3d vector.
  • p{i}vel[xyz]: Player i's velocity as a 3d vector.
  • p{i}_boost: Player i's boost remaining, in [0, 100]. A player can consume boost to substantially increase their speed, and is required to fly up into the z dimension.

Note: All p{i} columns will be NaN if and only if the player is demolished (destroyed by an enemy player; will respawn within a few seconds).

Distribution of team players:

  • Team_A: players 0, 1, 2
  • Team_B: players 3, 4, 5

Note: The orientation vector of the player's car (which way the car is facing) does not necessarily match the player's velocity vector, and this dataset does not capture orientation data.

For i in [0, 6):

  • boost{i}_timer: Time in seconds until big boost orb i respawns, or 0 if it's available. Big boost orbs grant a full 100 boost to a player driving over it.

Note: The orb (x, y) locations are roughly [(-61.4, -81.9),(61.4, -81.9),(-71.7, 0),(71.7, 0),(-61.4, 81.9),(61.4, 81.9)] with z = 0. (Players can also gain boost from small boost pads across the map).

  • player_scoring_next (train only): Which player scores at the end of the current event.
  • team_scoring_next (train only): Which team scores at the end of the current event (A or B), or NaN if the event does not end in a goal.

Target Columns

  • team_[A|B]_scoring_within_10sec (train only): Value of 1 if team_scoring_next == [A|B] and time_before_event is in [-10, 0], otherwise 0.
  • id (test only): Unique identifier for each test row. Each id allow to evaluate the corresponding predictions on this competence. Probability predictions for each id, where your predictions can range the real numbers from [0, 1].

Data Precision¶

The memory usage of complete dataset is ~10 Gb. It is proposed to sacrify precision from certain data in order to reduce this usage. Specifically using the method proposed here, the change per column will be the following, using pandas library:

In [1]:
dtypes = {'game_num': 'int32', 'event_id': 'int32', 
          'event_time': 'float16', 'ball_pos_x': 'float16', 
          'ball_pos_y': 'float16', 'ball_pos_z': 'float16',
          'ball_vel_x': 'float16', 'ball_vel_y': 'float16', 
          'ball_vel_z': 'float16', 'p0_pos_x': 'float16', 
          'p0_pos_y': 'float16', 'p0_pos_z': 'float16',
          'p0_vel_x': 'float16', 'p0_vel_y': 'float16', 
          'p0_vel_z': 'float16', 'p0_boost': 'float16',
          'p1_pos_x': 'float16', 'p1_pos_y': 'float16', 
          'p1_pos_z': 'float16', 'p1_vel_x': 'float16', 
          'p1_vel_y': 'float16', 'p1_vel_z': 'float16', 
          'p1_boost': 'float16', 'p2_pos_x': 'float16', 
          'p2_pos_y': 'float16', 'p2_pos_z': 'float16', 
          'p2_vel_x': 'float16', 'p2_vel_y': 'float16', 
          'p2_vel_z': 'float16', 'p2_boost': 'float16',
          'p3_pos_x': 'float16', 'p3_pos_y': 'float16', 
          'p3_pos_z': 'float16', 'p3_vel_x': 'float16', 
          'p3_vel_y': 'float16', 'p3_vel_z': 'float16', 
          'p3_boost': 'float16', 'p4_pos_x': 'float16', 
          'p4_pos_y': 'float16', 'p4_pos_z': 'float16', 
          'p4_vel_x': 'float16', 'p4_vel_y': 'float16', 
          'p4_vel_z': 'float16', 'p4_boost': 'float16',
          'p5_pos_x': 'float16', 'p5_pos_y': 'float16', 
          'p5_pos_z': 'float16', 'p5_vel_x': 'float16', 
          'p5_vel_y': 'float16', 'p5_vel_z': 'float16', 
          'p5_boost': 'float16', 'boost0_timer': 'float16', 
          'boost1_timer': 'float16', 'boost2_timer': 'float16', 
          'boost3_timer': 'float16', 'boost4_timer': 'float16', 
          'boost5_timer': 'float16', 'player_scoring_next': 'int8', 
          'team_scoring_next': 'object',
          'team_A_scoring_within_10sec': 'int8', 
          'team_B_scoring_within_10sec': 'int8'}
In [2]:
import pandas as pd

train_df = pd.DataFrame()

for i in range(10):
    temp_df  = pd.read_csv(f'../input/tabular-playground-series-oct-2022/train_{i}.csv', dtype=dtypes) # memory usage: 440.7+ MB, 264.4+ MB new
    train_df = pd.concat([train_df, temp_df], ignore_index=True)
    del temp_df
    print(f'train {i}')
train 0
train 1
train 2
train 3
train 4
train 5
train 6
train 7
train 8
train 9
In [4]:
train_df.info(verbose=False)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 21198036 entries, 0 to 21198035
Columns: 61 entries, game_num to team_B_scoring_within_10sec
dtypes: float16(55), int32(2), int8(3), object(1)
memory usage: 2.5+ GB
  • The train[0-9] dataset complete dataset use ~3 Gb, less than a half of the original data.
  • It is possible to save the complete dataset under and efficient format (e.g. hdf). To save loading time.
train_df = pd.concat([train0_df, train1_df, train2_df, train3_df, train4_df, 
           train5_df, train6_df, train7_df, train5_df, train6_df, train7_df])
train_df.reset_index(inplace=True)

train_df.to_hdf('train_complete_v1.hdf', key='df', mode='w')

Note: Each event consists of a chronological series of frames recorded at 10 frames per second. However, Rocket League is a fast game, but it do not involve relativistic particles. An adequate downsampling from the train dataset do not neccesarilly represent a mistake and can help to reduce model training / data analysis.

In this specific work the selected downsampling transform the 10 frames per second recorded to ~3 frames per second. Given the chronological order of the dataset rows, this can be done via df = df[::4].

Exploratory Data Analysis

¶

In [7]:
import numpy as np
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
In [8]:
print('Number of games:  ', len(train_df['game_num'].unique()))
print('Number of events: ', len(train_df['event_id'].unique()))
Number of games:   7365
Number of events:  30528
  • Each event represents a collection of gameplay snapshots that share similar caracteristics (time, player/ball position/velocity similarities etc). This indicate that it is needed to implement cross-validation or and adequate train-val distribution in out ML model.

Graphic representation of events¶

In [11]:
from time import sleep
from IPython.display import display, clear_output


def show_event(event_id):
    f,ax=plt.subplots(figsize=(13,5))

    temp2 = train_df[(train_df['event_id']==event_id)] #&(train_df['team_A_scoring_within_10sec']==1)]
    
    def plot_obj(x):
        t1 = temp2.iloc[x]

        ax.text(-130,70, str(t1['event_time']) + ' sec')
        
        for p in [0,1,2]:
            ax.scatter(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x'], color='darkred')
            ax.text(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x']+5, f'Player{p}')
            ax.arrow(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x'],t1[f'p{p}_vel_y']//3,t1[f'p{p}_vel_x']/3, width= 1 ,color='darkred')
        
        for p in [3,4,5]:
            ax.scatter(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x'], color='navy')
            ax.text(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x']+5, f'Player{p}')
            ax.arrow(t1[f'p{p}_pos_y'],t1[f'p{p}_pos_x'],t1[f'p{p}_vel_y']//3,t1[f'p{p}_vel_x']/3, width= 1,color='navy')

        ax.scatter(t1['ball_pos_y'],t1['ball_pos_x'], color='black',s=60)
        ax.text(t1['ball_pos_y'],t1['ball_pos_x']+5, f'Ball')
        ax.arrow(t1['ball_pos_y'],t1['ball_pos_x'],t1['ball_vel_y']/3,t1['ball_vel_x']/3, width= 1, color='black')

    for i in range(temp2.shape[0]//4):
        ax.clear()
        ax.set_ylim([-100,100]); ax.set_xlim([-140,140])
        ax.vlines([-105,105],color='green', ymin=-82, ymax=82)
        
        #ax.hlines([0],color='black', xmin=-105, xmax=105, ls='dashed', lw=1)
        
        ax.text(-125,0,'Score for\nTeam B')
        ax.text(110,0,'Score for\nTeam A')
        ax.hlines([-82,82],color='green', xmin=-105, xmax=105)
        
        plot_obj(i*4)
        display(f)
        clear_output(wait = True)
In [12]:
show_event(event_id=1002)
  • Velocity vector indicate the direction of movement for players / ball.
  • If the ball enters the left score zone, Team_B wins a score. Team_A scores in the right zone.

Position of players when a Team won¶

In [8]:
def plot_player_positions(data_temp, t_title, cmin=0, cmax=1700):
    # fill values in order to plot, do not consider it much
    for i in range(0,6):
        for p in 'xyz':
            data_temp[f'p{i}_pos_{p}'].fillna(value=-150, inplace=True) # outside field
            data_temp[f'p{i}_vel_{p}'].fillna(value=0, inplace=True)
        data_temp[f'p{i}_boost'].fillna(value=0, inplace=True)

    range_x = np.linspace(-105,105,106)
    range_y = np.linspace(-82,82,83)
    
    f,ax=plt.subplots(ncols=2, figsize=((105*2)//14,(82)//14))
    h0 = ax[0].hist2d(pd.concat([data_temp['p0_pos_y'],data_temp['p1_pos_y'],data_temp['p2_pos_y']]), 
                      pd.concat([data_temp['p0_pos_x'],data_temp['p1_pos_x'],data_temp['p2_pos_x']]), 
                      bins=[range_x,range_y],cmax=cmax, cmin=cmin)

    h1 = ax[1].hist2d(pd.concat([data_temp['p3_pos_y'],data_temp['p4_pos_y'],data_temp['p5_pos_y']]),
                      pd.concat([data_temp['p3_pos_x'],data_temp['p4_pos_x'],data_temp['p5_pos_x']]), 
                      bins=[range_x,range_y],cmax=cmax, cmin=cmin)
    
    #f.colorbar(h0[3],ax=ax[0])
    #f.colorbar(h1[3],ax=ax[1])
    for i in range(2):
        ax[i].set_xlabel('y axis')
        ax[i].set_ylabel('x axis')
    
    ax[0].set_title('Team A players')
    ax[1].set_title('Team B players')
    
    f.suptitle(t_title, fontsize=16)
    plt.show()
In [9]:
data_A = train_df[(train_df['team_A_scoring_within_10sec']==1)].copy()
data_B = train_df[(train_df['team_B_scoring_within_10sec']==1)].copy()

plot_player_positions(data_A, 'Team A scoring')
In [10]:
# Team B winner
plot_player_positions(data_B, 'Team B scoring')
  • The players follow specific paths frequently in order to recover boost, or some of the 6's great boost orbs. This can be the general strategy.
  • An specific difference its that a loser team protect more -> more close to its score zone.

No scoring¶

plot_player_positions(data_N,cmin=0, cmax=45000)

image.png

  • Even at no scoring, the players tend to use specific paths to recover boost.

Check position of the boost orbs¶

In [83]:
data = train_df[train_df['boost5_timer']==0].copy()
plot_player_positions(data, 'Boost5 position', cmin=0, cmax=40000)
  • This can be tricky; luckily, if a boost orb is available ('boost_timer'==0), it's because players aren't going through that position, and it can be checked using a 2d histogram.

Then this is the distribution of orbs.

In [85]:
### Estimation of boost position

x= [-61.4, 61.4, -71.7, 71.7, -61.4, 61.4]
y= [-81.9, -81.9, 0, 0, 81.9, 81.9]

f,ax=plt.subplots()
ax.plot(y,x,marker='o',lw=0)

for i in range(6):
    ax.text(y[i],x[i],f'boost {i}',fontsize=20)

Score Zones from ball position¶

In [11]:
# define distance from center to ball position to avoid kickoff snapshots
train_df['dist_center'] = np.linalg.norm(train_df[['ball_pos_x','ball_pos_y','ball_pos_z']], axis=1)

# set scoring scenarios
data_A = train_df[(train_df['team_A_scoring_within_10sec']==1)&(train_df['ball_pos_x']!=0)&(train_df['dist_center']>10)]
data_B = train_df[(train_df['team_B_scoring_within_10sec']==1)&(train_df['ball_pos_x']!=0)&(train_df['dist_center']>10)]
In [43]:
def contour_ball_positions(data_A, data_B):#,cmin=0,cmax=1700):

    range_x = np.linspace(-105,105,106)
    range_y = np.linspace(-82,82,83)
    range_z = np.linspace(0,80,42)
    
    fig,ax=plt.subplots(figsize=(15,10), ncols=2, nrows=2)
    
    for cnt, data_temp in enumerate([data_A, data_B]):
        h = np.histogram2d(data_temp['ball_pos_y'], data_temp['ball_pos_x'],bins=[range_x,range_y])
        Y, X = np.meshgrid(h[1][:-1], h[2][:-1], indexing='ij')
        cp = ax[0][cnt].contourf(Y, X, h[0], levels=6)
        fig.colorbar(cp, ax=ax[0][cnt])
        
        h = np.histogram2d(data_temp['ball_pos_y'], data_temp['ball_pos_z'],bins=[range_x,range_y])
        Y, Z = np.meshgrid(h[1][:-1], h[2][:-1], indexing='ij')
        cp = ax[1][cnt].contourf(Y, Z, h[0], levels=5)
        fig.colorbar(cp, ax=ax[1][cnt])
    
    for i in range(2):
        for j in range(2):
            ax[i][j].set_xlim([-105,105])
    
    ax[0][0].set_ylim([-82,82])
    ax[0][1].set_ylim([-82,82])
    
    ax[1][0].set_ylim([0,80])
    ax[1][1].set_ylim([0,80])
        
    ax[0][0].set_title('Team A scoring')
    ax[0][1].set_title('Team B scoring')
    return fig, ax
f, ax = contour_ball_positions(data_A, data_B)

# (x**2)/a**2 + (y**2)/b**2 + (z)**2/c**2 = r**2+y_shift

def plot_sphr(a,b,c,y_shift):
    x_r_pos = lambda y:  np.sqrt(1 - (y - y_shift)**2/b**2)*a
    x_r_neg = lambda y: -np.sqrt(1 - (y - y_shift)**2/b**2)*a
    z_r_pos = lambda y:  np.sqrt(1 - (y - y_shift)**2/b**2)*c
    #z_r_neg = lambda y: -np.sqrt(1 - (y)**2/b**2)*c**2
    # inverted
    x_r_pos_inv = lambda y:  np.sqrt(1 - (y + y_shift)**2/b**2)*a
    x_r_neg_inv = lambda y: -np.sqrt(1 - (y + y_shift)**2/b**2)*a
    z_r_pos_inv = lambda y:  np.sqrt(1 - (y + y_shift)**2/b**2)*c
    
    y0 = np.linspace(-b+y_shift, b+y_shift,20)
    y1 = np.linspace(-b-y_shift, b-y_shift,20)
    
    #for cnt, y in enumerate([y0,y1]):
    ax[0][0].plot(y0, x_r_pos(y0), color='darkred')
    ax[0][0].plot(y0, x_r_neg(y0), color='darkred')
    ax[1][0].plot(y0, z_r_pos(y0), color='darkred')
    
    ax[0][1].plot(y1, x_r_pos_inv(y1), color='darkred')
    ax[0][1].plot(y1, x_r_neg_inv(y1), color='darkred')
    ax[1][1].plot(y1, z_r_pos_inv(y1), color='darkred')
    
    
plot_sphr(a=196, b=75, c=30.25,  y_shift = 105)
plot_sphr(a=36,  b=40, c=18, y_shift = 105)
plot_sphr(a=22,  b=16, c=13,  y_shift = 105)

ax[0][1].text(-100, 0, s='A0', color='white', fontsize=20)
ax[0][1].text( -80, 0, s='A1', color='white', fontsize=20)
ax[0][1].text( -50, 0, s='A2', color='white', fontsize=20)

ax[0][0].text(90,0, s='A3', color='white', fontsize=20)
ax[0][0].text(70, 0, s='A4', color='white', fontsize=20)
ax[0][0].text(40, 0, s='A5', color='white', fontsize=20)

for i in range(2):
    ax[0][i].set_ylabel('x axis')
    ax[1][i].set_ylabel('z axis')
    for j in range(2):
        ax[j][i].set_xlabel('y axis')

plt.tight_layout()
  • A countour plot of the ball position at scoring scenarios indicate that there is a distribution around score zone. Some kind of sphereoid can enclose the visible zones, however, this is an inexact knowlege that can affect the accuracy of a model, then it is better to just let the ML model train without them.

Note: Center point (0,0) is also a representative position to score both teams. This is that a score can occur at kickoff.

Event analysis¶

Each event is composed by a collection of snapshots recorded with a regular rate, but entire time series has variable length for each event.

In [12]:
f,ax=plt.subplots(figsize=(10,5))
temp = train_df[['event_id','event_time']].groupby('event_id').first()['event_time'].abs()
temp.hist(bins=100, ax=ax)
ax.set_title('Event time distribution')
ax.set_xlabel('duration [seconds]')

print('Maximum Duration for a particular event: ', f'{temp.max()} secs')
Maximum Duration for a particular event:  920.0 secs

Considering the target features, lets check the distribution of the snapshots results.

In [13]:
temp2 = train_df[['event_id','team_A_scoring_within_10sec','team_B_scoring_within_10sec']].groupby('event_id').last()
temp2 = temp2.sum(axis=1)

f,ax=plt.subplots(figsize=(10,3))
ax.scatter(temp.values, temp2.values, marker='.')

ax.set_xlabel('event duration [sec]')
ax.set_ylabel('Score or not')

ax.set_title('Event Final result distribution')
Out[13]:
Text(0.5, 1.0, 'Event Final result distribution')
  • The events that last more that ~300 secs end in some Team scoring (==1) on the train dataset.

Note: as the 'positive' cases in which a Team scores are registered in the last 10 seconds, the longer the event, the more imbalance the target distribution of the snapshots.

Imbalance Dataset management¶

In [14]:
counts = train_df[['team_A_scoring_within_10sec','team_B_scoring_within_10sec']].value_counts()
counts = pd.DataFrame(counts)
nrows  = train_df.shape[0]

counts / nrows
Out[14]:
0
team_A_scoring_within_10sec team_B_scoring_within_10sec
0 0 0.887607
1 0 0.057083
0 1 0.055310

Our Dataset has a representative imbalance data, ~88.7% of the snapshots represent an scenario in which there is no score within the next 10 secs. To attenuate this, the possible next steps are:

  • Downsampling (0,0) case (no scoring scenario)
  • Oversampling (1,0)/(0,1) case (scoring scenario)
  • Define Class weigths in our training model to correctly estimate training metrics.
  • Data Augmentation over (1,0)/(0,1)

In the case of this work, Data Agumentation was selected, this was possible due to:

  • Reflections over x and y axis.
  • Permutation of players in the same Team.

Since a players strategy need to be independent of coordinate system, reflection of players and associated features are possible. But remember, that score zones are defined at an specific side of the field, then, reflections around X-axis of associated features are going to also interchange the name of those features with the oponent Team. Team_A features <-> Team_B features.

Gravity for ball¶

Inside game physics, it is expected that there is some kind of gravity that control player/ball pos z parameter.

Vt+1=Vt+gΔtVt+1=Vt+gΔt, then: Vt+1−VtΔt=gVt+1−VtΔt=g, in this notation g<0g<0.

In [53]:
def check_acc(event_id=1003, parameter='ball_vel_z' ):
    data_temp  = train_df[train_df['event_id']==event_id]
    event_time = data_temp['event_time'] - data_temp.iloc[0]['event_time']
    #data_temp
    delta_time       = event_time - event_time.shift(1)
    ball_vel_z_shift = data_temp[parameter].shift(1)
    acc = (data_temp[parameter] - ball_vel_z_shift)/delta_time
    
    d = acc[~(acc.isnull())&~(acc.isin([np.inf, -np.inf]))]
    
    return d

c = pd.DataFrame()

for cnt, el in enumerate(train_df['event_id'].unique()):
    t = check_acc(event_id=el, parameter='ball_vel_z' )
    c = pd.concat([c, t], ignore_index=True)
    del t

# Freq distribution
r = c[0].value_counts()  # 26274 values
r = pd.DataFrame(r)
In [55]:
t = r.sort_values(by=0, ascending=False).iloc[1:4]
s = t.index.values * t[0].values
print('Estimated ball gravity', s.sum()/ t[0].values.sum())
Estimated ball gravity -10.868541850875738
In [57]:
######## ball

f, ax=plt.subplots()
range_x = np.linspace(-17, -4, 300)
c.hist(bins=range_x, ax=ax)
ax.set_title('Estimated gravity distribution')
plt.show()
In [50]:
######## player 0

f, ax=plt.subplots()
range_x = np.linspace(-17, -4, 400)
c.hist(bins=range_x, ax=ax)
ax.set_title('Estimated gravity distribution')
plt.show()
  • The gravity obtained indicate that ball has an specific gravity different from the players gravity.
  • Using gravity data and position/velocity data of ball allows to estimate distanve traveled from trajectory equation. z=z0+vzt+0.5gt2z=z0+vzt+0.5gt2

Ball trajectory on z-azis along time¶

In [58]:
data_temp = train_df[train_df['event_id']==1003].copy()
data_temp['event_time'] -= data_temp.iloc[0]['event_time']

f,ax=plt.subplots(figsize=(15,4))
data_temp.plot('event_time', 'ball_pos_z', ax=ax)

ax.set_ylabel('pos_z')
ax.set_title('Variation of ball position at z-axis on event 1003')
ax.hlines([0],xmin=-10,xmax=140,color='darkgreen')
Out[58]:
<matplotlib.collections.LineCollection at 0x7f431caf9e10>
  • The trajectory of the ball cointains frequent changes on pos-z, due to collision with players and environment. Most part of the time the ball is bouncing, probably there is friction is the ground field but it negligible to the trajectory estimation.
  • Ball never reach z=0, cause the position seems to be estimated from the center of the ball.
  • Ball position of z axis along a event is composed by a group of parabolic movements.

Distribution of players that won a score¶

In [62]:
f,ax=plt.subplots()
data_temp = train_df[train_df['event_time']>-10]
data_temp = data_temp.groupby(['event_id']).last()
data_temp['player_scoring_next'].value_counts().plot(kind='bar', ax=ax)
ax.set_title('Distribution of players scoring')
ax.set_xlabel('Player')
ax.set_ylabel('Number of Events')
Out[62]:
Text(0, 0.5, 'Number of Events')
  • The distribution of players that score (1-5) is relatively uniform, except for the case that no player score at the event (-1 bar) that seems to have a representative difference.

Distribution of events where a Team Score¶

In [63]:
data_temp = train_df[train_df['event_time']>-10]
data_temp = data_temp.groupby(['event_id']).last().groupby('team_scoring_next')
data_temp.size().plot(kind='pie', autopct='%.2f')
Out[63]:
<AxesSubplot:ylabel='None'>
  • The distribution of scoring events is relatively uniform between both Teams.

Time frame recorded frequency¶

In [114]:
import random

events   = train_df['event_id'].unique()
l_events = random.choices(events, k=4)


f,ax=plt.subplots(ncols=2, nrows=2, figsize=(10,6))
for i, event in enumerate(l_events):
    t = train_df[train_df['event_id']==event]['event_time']
    t = (t - t.shift(1))
    range_x = np.linspace(0.05, 0.15, 20)
    ax[i//2][i%2].hist(t, bins=range_x)
    ax[i//2][i%2].set_title(f'Event: {event}')

f.suptitle('Time frame recorded frequency distribution', fontsize=16)
f.tight_layout()
  • A subsequent snapshots series of a event do not neccesarily have the same record time interval. The time frame recorder rate is roughly close to 0.1

Demolished players can affect the event score?¶

In [197]:
train_df['dem_p_A'] = train_df[['p0_pos_x', 'p1_pos_x','p2_pos_x']].isnull().astype(int).sum(axis=1)
train_df['dem_p_B'] = train_df[['p1_pos_x', 'p2_pos_x','p3_pos_x']].isnull().astype(int).sum(axis=1)
train_df['enc']     = -1*train_df['team_A_scoring_within_10sec'] + train_df['team_B_scoring_within_10sec']

t = train_df[(train_df['event_time']>-10)][['event_id','dem_p_A','dem_p_B','enc']].groupby('event_id').sum()
t.loc[t['enc']<0, 'enc'] = -1 ; t.loc[t['enc']>0, 'enc'] =  1
t.loc[:,['dem_p_A','dem_p_B']] = np.ceil(t[['dem_p_A','dem_p_B']]*0.02).astype('int8')
In [214]:
f,ax=plt.subplots(ncols=3, figsize=(10,4))
titles = ['None','A','B']
for cnt, i in enumerate([0,-1,1]):
    t[t['enc']==i][['dem_p_A','dem_p_B']].value_counts().plot(kind='bar', ax=ax[cnt])
    ax[cnt].set_title(f'Team scoring: {titles[cnt]}')
f.tight_layout()
  • At the last 10s secs of the events is common that no players are being demolished ( (0,0) bar).
  • The distribution of demolished players in the last 10 secs do not seems to have a relation that can be determined visually from the graphs. Using demolished players per Team as a feature in the model do not show improvements.

Feature Engineering¶

As in any model, an adequate selection of features represent a high adantage. In the case of this work the following features where selected:

  • Position and Velocity columns for all players and ball,
  • Boost timer and boost level per player.

The following columns were ignored:

  • player_scoring_next and team_scoring_next

Since the target columns represent one-hot enconded columns of the results, a new target column need to be included in order to represent the three possible outcomes.

  • team_A_scoring_within_10sec, team_B_scoring_within_10sec, team_Null

New Features¶

The following set of features represent a great improvement in evaluation metrics of the model.

  • Magnitud of the velocity vectors: players and ball
  • Magnitud of the position vectors: players and ball, this is equivalent to calculate the distance from players/ball position and center of the field.
  • Distance each player and ball
  • Diference between velocity vectors: between ball and each player
  • Azimuth and Elevation angles for ball: using velocity vector as an direction vector is possible to obtain those direction angles.

img

  • Minimum players distance to the ball 1 sec into the future as much: looking at the Euclidean distance formula, and considering a linear approximation of it as a funcion of time, the future distances can be predicted.

√(X′player−X′ball)2+(Y′player−Y′ball)2+(Y′player−Y′ball)2=(Xplayer′−Xball′)2+(Yplayer′−Yball′)2+(Yplayer′−Yball′)2=

Let: [X/Y/Z]pb=[X/Y/Z]player−[X/Y/Z]ball[X/Y/Z]pb=[X/Y/Z]player−[X/Y/Z]ball, V[X/Y/Z]pb=V[X/Y/Z]player−V[X/Y/Z]ballV[X/Y/Z]pb=V[X/Y/Z]player−V[X/Y/Z]ball. Then: f(t)=√(Xpb+VXpbt)2+(Ypb+VYpbt)2+(Ypb+VYpbt)2f(t)=(Xpb+VXpbt)2+(Ypb+VYpbt)2+(Ypb+VYpbt)2

To find the time tt in which the distance to the ball is minimum the function above can be minimized, then t is truncated at 1 if t>1.

  • Angle Positioning: players and ball

In our model, the direct implementation of position and velocity columns seems to not help as much as angle positioning according to evaluation metrics.

As reference points, the 'soccer' goal vertices are used.

In [14]:
import numpy as np
import matplotlib.pyplot as plt

range_x = np.linspace(-20,20,20*3)
range_z = np.linspace(0,15, 15*3)

t = train_df[(train_df['team_A_scoring_within_10sec']==1)&(train_df['ball_pos_y']>102)]
f,ax=plt.subplots(ncols=2, figsize=(18,5))
ax[0].hist2d(t['ball_pos_x'],t['ball_pos_z'], bins=[range_x, range_z], cmax=100)

t = train_df[(train_df['team_B_scoring_within_10sec']==1)&(train_df['ball_pos_y']<-102)]
ax[1].hist2d(t['ball_pos_x'],t['ball_pos_z'], bins=[range_x, range_z], cmax=100)

for i in range(2):
    ax[i].vlines([-16.6,16.6], ymin=0, ymax=15, color='darkred')
    ax[i].hlines([1.6,11.3], xmin=-20, xmax=20, color='darkred')

ax[0].set_title('Team A side') ; ax[1].set_title('Team B side')
f.suptitle('Rocket League goal vertices', fontsize=20)
plt.show()

print('Vertices: ', (16.6, 1.6), (-16.6, 1.6), (16.6, 11.4), (-16.6, 11.4))

####################33### xy plane
temp = np.array([-16.6, 100.6])-train_df[['ball_pos_x','ball_pos_y']]
temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
train_df['alpha_1'] = np.degrees(np.arccos(temp)).astype('float16')

temp = np.array([16.6, 100.6])-train_df[['ball_pos_x','ball_pos_y']]
temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
train_df['alpha_2'] = np.degrees(np.arccos(temp)).astype('float16')

temp = np.array([-16.6, -100.6])-train_df[['ball_pos_x','ball_pos_y']]
temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
train_df['beta_1'] = -np.degrees(np.arccos(temp)).astype('float16')

temp = np.array([16.6, -100.6])-train_df[['ball_pos_x','ball_pos_y']]
temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
train_df['beta_2'] = -np.degrees(np.arccos(temp)).astype('float16')
Vertices:  (16.6, 1.6) (-16.6, 1.6) (16.6, 11.4) (-16.6, 11.4)
In [108]:
import matplotlib

f, ax=plt.subplots(figsize=(10,5))

t = train_df.iloc[1600]
ax.scatter(t['ball_pos_y'],t['ball_pos_x'],color='red')
ax.plot([t['ball_pos_y'], 100.6],[t['ball_pos_x'],-16.6], color='navy') ; ax.plot([t['ball_pos_y'], 100.6],[t['ball_pos_x'], 16.6], color='navy')
ax.plot([t['ball_pos_y'], -100.6],[t['ball_pos_x'],-16.6], color='navy') ; ax.plot([t['ball_pos_y'], -100.6],[t['ball_pos_x'], 16.6], color='navy')

ax.set_xlim([-102, 102])
ax.set_ylim([-82, 82])


print('alpha  1:', t['alpha_1'])
print('alpha  2:', t['alpha_2'])

print('beta 1:', t['beta_1'])
print('beta 2:', t['beta_2'])


ax.add_patch(matplotlib.patches.Arc((-25, -40), 40, 60, angle=0,
             theta1=-185, theta2=-90, 
             edgecolor='black', lw=1.1))

ax.add_patch(matplotlib.patches.Arc((-25, -20), 40, 60, angle=0,
             theta1=-185, theta2=-90, 
             edgecolor='black', lw=1.1))


ax.add_patch(matplotlib.patches.Arc((75, -40), 40, 60, angle=0,
             theta1=-90, theta2=30, 
             edgecolor='black', lw=1.1))

ax.add_patch(matplotlib.patches.Arc((75, -20), 40, 60, angle=0,
             theta1=-90, theta2=50, 
             edgecolor='black', lw=1.1))


ax.text(85, -70, 'alpha_1') ; ax.text(80, 10, 'alpha_2')
ax.text(-25, -70, 'beta_1') ; ax.text(-25, -40, 'beta_2')

ax.vlines([-25],ymin=-75, ymax=-30, color='black')
ax.vlines([75],ymin=-75, ymax=-35, color='black')

ax.arrow(t['ball_pos_y'],t['ball_pos_x'],t['ball_vel_y']//2,t['ball_vel_x']//2, width= 1,color='black')

ax.set_title('Angle Direction on xy axis (alpha & beta)')
alpha  1: 143.2
alpha  2: 154.0
beta 1: -111.8
beta 2: -121.56
Out[108]:
Text(0.5, 1.0, 'Angle Direction on xy axis (alpha & beta)')

Alpha and Beta angles are state to reference from the xy data, similarly a sigma/gamma angles can reference data from yz plane.

Finally, each snapshot row on the train dataset have 118 features and 3 target columns.

Model Architecture¶

The selected model architecture was a 4 layer model,and categorical_crossentropy loss.

image.png

In order to obtain the better evaluation metrics, the following preprocessing was applied:

  1. Furtunately, Data Augmentation over scoring snapshots can turns imbalance dataset into balance dataset.
In [1]:
import pandas as pd

temp = pd.read_hdf('../input/tbsoct2022-pro-data/train_0.25_v2.hdf', mode='r', key='df')
In [6]:
import matplotlib.pyplot as plt

f,ax=plt.subplots()
temp[['team_A_scoring_within_10sec','team_B_scoring_within_10sec']].value_counts().plot(kind='pie',autopct='%1.1f%%')
ax.set_title('Now we have a Balanced Dataset', fontsize=20)
Out[6]:
Text(0.5, 1.0, 'Now we have a Balanced Dataset')
  1. As explainend before, cross validation is needed due to all snapshots from the same event are relate, GroupShuffleSplit can do the trick, and complete events are stated for train and test dataset
  2. As all features are numeric StandardScalet preprocessing was applied to Data.
In [ ]:
# trayectory of a ball
g = 12.39 # gravity

def dits_ball_hor(v_y, v_z, z, g=g):
    t1 =  ( v_z + np.sqrt(v_z**2 + 2*z*g) )/ g
    return abs(v_y*t1)

dits_ball_hor(v_y=-4, v_z=25, z=100, g=g)

Model Training and Prediction

¶

In [8]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
  • Feature Engineering
In [18]:
# Feature Enginerring

def feature_eng(df):
    # I set the player far away from both zone score
    for i in range(6):
        df[f'p{i}_pos_x'].fillna(value= 85, inplace=True)
        df[f'p{i}_pos_y'].fillna(value=  0, inplace=True)
        df[f'p{i}_pos_z'].fillna(value=  0, inplace=True)
        for p in 'xyz':
            df[f'p{i}_vel_{p}'].fillna(value=0, inplace=True)
            df[f'p{i}_boost'].fillna(value=0, inplace=True)
    
    ############################################### aximuth angle
    df['azimuth'] = np.degrees(np.arctan(df['ball_vel_x']/df['ball_vel_y'])) + 90
    df.loc[(df['ball_vel_y']<0),'azimuth'] -= 180
    # filla
    df['azimuth'].fillna(value=0, inplace=True)
    df['azimuth'] = df['azimuth'].astype('float16')
    print('azimuth')
    
    ############################################### ball velocity
    df['ball_vel']   = np.linalg.norm(df[['ball_vel_x', 'ball_vel_y','ball_vel_z']], axis=1).astype('float16')
    #df['bal_vel_xy'] = np.linalg.norm(df[['ball_vel_x', 'ball_vel_y']], axis=1).astype('float16')
    temp_mod = np.linalg.norm(df[['ball_vel_x', 'ball_vel_y']], axis=1).astype('float16')
    
    for i in range(6):
        df[f'p{i}_vel'] = np.linalg.norm(df[[f'p{i}_vel_x', f'p{i}_vel_y',f'p{i}_vel_z']], axis=1).astype('float16')
    print('ball & players vel')

    ############################################### elevation
    #df['elevation'] = (df['ball_vel_x']**2 + df['ball_vel_y']**2)/(df['bal_vel_xy'] * df['ball_vel'])
    df['elevation'] = (df['ball_vel_x']**2 + df['ball_vel_y']**2)/(temp_mod * df['ball_vel'])
    # fill nan values in order to fill elevation with 0 ( cos-1(1)=0 )
    df['elevation'].fillna(value=1, inplace=True)
    df.loc[(df['elevation']>1),'elevation']  =  1
    df.loc[(df['elevation']<-1),'elevation'] = -1
    df['elevation']  = np.degrees(np.arccos(df['elevation']))

    df.loc[(df['ball_vel_x']==0) & (df['ball_vel_y']==0) & (df['ball_vel_z']!=0), 'elevation'] = 90
    df.loc[(df['ball_vel_x']==0) & (df['ball_vel_y']==0) & (df['ball_vel_z']==0), 'elevation'] = 0
    df.loc[df['ball_vel_z']<0,'elevation'] *= -1
    df['elevation'] = df['elevation'].astype('float16')
    print('elevation')
    
    del temp_mod
    
    ############################################### distance player-ball
    for i in range(6):
        df[f'dist_p{i}_ball'] = np.linalg.norm(df[['ball_pos_x','ball_pos_y','ball_pos_z']].to_numpy() - 
                                               df[[f'p{i}_pos_x',f'p{i}_pos_y',f'p{i}_pos_z']].to_numpy(), axis=1)
    print('dist_p[0-5]_ball')
    
    ############################################### dist center ball 
    df['dist_ball'] = np.linalg.norm(df[['ball_pos_x','ball_pos_y','ball_pos_z']], axis=1).astype('float16')
    
    for i in range(6):
        df[f'dist_p{i}_center'] = np.linalg.norm(df[[f'p{i}_pos_x',f'p{i}_pos_y',f'p{i}_pos_z']], axis=1).astype('float16')
    print('dist_center, ball & players')
    
    ################################################ angle from zone scoring
    
    ### xy plane
    temp = np.array([-16.6, 100.6])-df[['ball_pos_x','ball_pos_y']]
    temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
    df['alpha_1'] = (np.degrees(np.arccos(temp))).astype('float16')
    
    temp = np.array([ 16.6, 100.6])-df[['ball_pos_x','ball_pos_y']]
    temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
    df['alpha_2'] = np.degrees(np.arccos(temp)).astype('float16')
    
    temp = np.array([-16.6,-100.6])-df[['ball_pos_x','ball_pos_y']]
    temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
    df['beta_1'] = -np.degrees(np.arccos(temp)).astype('float16')
    
    temp = np.array([ 16.6,-100.6])-df[['ball_pos_x','ball_pos_y']]
    temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
    df['beta_2'] = -np.degrees(np.arccos(temp)).astype('float16')
    
    ##### yz plane

    temp = np.array([ 100.6, 11.3]) - df[['ball_pos_y','ball_pos_z']]
    temp = -temp.iloc[:,1]/np.linalg.norm(temp, axis=1)
    df['sigma_2'] = (np.degrees(np.arccos(temp))-90).astype('float16')

    temp = np.array([-100.6, 11.3]) - df[['ball_pos_y','ball_pos_z']]
    temp = -temp.iloc[:,1]/np.linalg.norm(temp, axis=1)
    df['gamma_2'] = (np.degrees(np.arccos(temp))-90).astype('float16')
    print('scoring zone angles')

def new_features(df):
    for i in range(6):
        temp = np.array([-16.6, 100.6])-df[[f'p{i}_pos_x',f'p{i}_pos_y']]
        temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
        df[f'p{i}_alpha_1'] = (np.degrees(np.arccos(temp))).astype('float16')

        temp = np.array([ 16.6, 100.6])-df[[f'p{i}_pos_x',f'p{i}_pos_y']]
        temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
        df[f'p{i}_alpha_2'] = (np.degrees(np.arccos(temp))).astype('float16')

        temp = np.array([-16.6,-100.6])-df[[f'p{i}_pos_x',f'p{i}_pos_y']]
        temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
        df[f'p{i}_beta_1']  = -np.degrees(np.arccos(temp)).astype('float16')

        temp = np.array([ 16.6,-100.6])-df[[f'p{i}_pos_x',f'p{i}_pos_y']]
        temp = -temp.iloc[:,0] / np.linalg.norm(temp, axis=1)
        df[f'p{i}_beta_2']  = -np.degrees(np.arccos(temp)).astype('float16')
        
        del temp
        print(i)

def new_features2(df):
    for i in range(6):
        temp = df[[f'p{i}_vel_x',f'p{i}_vel_y',f'p{i}_vel_z']] - df[['ball_vel_x','ball_vel_y','ball_vel_z']].to_numpy()
        df[f'dif_p{i}'] = np.linalg.norm(temp, axis=1).astype('float16')
        del temp
        
        ### minimum distance time
        diff_pos = df[[f'p{i}_pos_x',f'p{i}_pos_y',f'p{i}_pos_z']].to_numpy() - df[['ball_pos_x','ball_pos_y','ball_pos_z']].to_numpy()
        diff_vel = df[[f'p{i}_vel_x',f'p{i}_vel_y',f'p{i}_vel_z']].to_numpy() - df[['ball_vel_x','ball_vel_y','ball_vel_z']].to_numpy()
        df[f't_p{i}']  = - (diff_pos * diff_vel).sum(axis=1)
        df[f't_p{i}'] /=  (diff_vel*diff_vel).sum(axis=1)
        df[f't_p{i}']  = df[f't_p{i}'].astype('float16')
        
        del diff_pos, diff_vel
        print(i)
    
    temp = df[['t_p0','t_p1','t_p2','t_p3','t_p4','t_p5']]
    time = temp.where(temp > 0).min(axis=1).fillna(0)
    time.loc[time>1.0] = 1.0
    
    # determine minimum distance on the next sec at much
    for i in range(6):
        diff_pos = df[[f'p{i}_pos_x',f'p{i}_pos_y',f'p{i}_pos_z']].to_numpy() - df[['ball_pos_x','ball_pos_y','ball_pos_z']].to_numpy()
        diff_vel = df[[f'p{i}_vel_x',f'p{i}_vel_y',f'p{i}_vel_z']].to_numpy() - df[['ball_vel_x','ball_vel_y','ball_vel_z']].to_numpy()
        
        df[f'mdist_p{i}'] = np.linalg.norm(diff_pos + diff_vel * time.to_numpy()[:, np.newaxis], axis=1).astype('float16')
        
        del diff_pos, diff_vel
        print(i)
    
    del time, temp
    
    cols = ['t_p0','t_p1','t_p2','t_p3','t_p4','t_p5']
    df.drop(cols, inplace=True, axis=1)


#feature_eng(df)
  • Data Augmentation: X & Y Reflections and Player Permutations
In [13]:
def reflexion_y_axis(df, pos_vel=True):
    t = df[(df['team_A_scoring_within_10sec']==1)|(df['team_B_scoring_within_10sec']==1)].copy()
    # 'ball_pos_x',
    t['ball_pos_x'] *= -1 
    
    if pos_vel: 
        # 'ball_vel_x' * -1
        t['ball_vel_x'] *= -1
        # 'p[0-6]_[pos/vel]_[x]' * -1
        for i in range(6):
            for el in ['pos','vel']:
                t[f'p{i}_{el}_x'] *= -1
    
    t['sec_ball_pos_x'] *= -1
    for i in range(6):
        t[f'sec_p{i}_pos_x'] *= -1
    
    # 'boost1_timer' <-> 'boost0_timer',  'boost5_timer' <-> 'boost4_timer'
    t.rename(columns={'boost1_timer': 'boost0_timer', 'boost0_timer': 'boost1_timer',
                      'boost5_timer': 'boost4_timer', 'boost4_timer': 'boost5_timer'}, inplace=True)
    # azimuth reflexion around y-axis  and angle scoring reflection
    for angle in ['azimuth','beta_1', 'beta_2', 'alpha_1', 'alpha_2']:
        t[angle] = t.apply(lambda x: 180-x[angle] - 360*(x[angle]<0), axis=1)
    
    return t
In [14]:
# reflection around x-axis
# Possible Features 'player_scoring_next', 'team_scoring_next'

def reflexion_x_axis(df, pos_vel=True):
    t = df[(df['team_A_scoring_within_10sec']==1)|(df['team_B_scoring_within_10sec']==1)].copy()
    # 'ball_pos_y'
    t['ball_pos_y'] *= -1 
    
    # ball_vel_y' * -1
    if pos_vel:
        t['ball_vel_y'] *= -1
        # 'p[0-5]_[pos/vel]_y' * -1
        for i in range(6):
            for el in ['pos','vel']:
                t[f'p{i}_{el}_y'] *= -1
    
    t['sec_ball_pos_y'] *= -1
    for i in range(6):
        t[f'sec_p{i}_pos_y'] *= -1
    
    # 'boost1_timer' <-> 'boost5_timer',  'boost0_timer' <-> 'boost4_timer'
    t.rename(columns={'boost1_timer': 'boost5_timer', 'boost5_timer': 'boost1_timer',
                      'boost0_timer': 'boost4_timer', 'boost4_timer': 'boost0_timer'}, inplace=True)
    # 'azimuth' * -1 and scoring zones angles
    for angle in ['azimuth','beta_1', 'beta_2', 'alpha_1', 'alpha_2']:
        t[angle] *= -1
    
    # 'dist_side0' <-> 'dist_side1'
    t.rename(columns={'dist_side0': 'dist_side1', 'dist_side1':'dist_side0'}, inplace=True)
    # 'ball_pos_side' * -1
    t['ball_pos_side'] *= -1
    
    # 'p[0-5]_pos_side' *-1
    for i in range(6):
        t[f'p{i}_pos_side'] *= -1
    
    # interchange fixed angles
    for i in range(2):
        t.rename(columns={f'alpha_{i}':f'beta_{i}', f'beta_{i}':f'alpha_{i}'}, inplace=True)
        t.rename(columns={f'sigma_{i}':f'gamma_{i}', f'gamma_{i}':f'sigma_{i}'}, inplace=True)
    
    ### change Team [0-2] <-> [3-5]
    for i in range(3):
        t.rename(columns={f'p{i}_vel' :f'p{i+3}_vel', f'p{i+3}_vel' :f'p{i}_vel'}, inplace=True)
        #t.rename(columns={f'p{i}_dist':f'p{i+3}_dist',f'p{i+3}_dist':f'p{i}_dist'}, inplace=True)
        t.rename(columns={f'dist_p{i}_center':f'dist_p{i+3}_center',f'dist_p{i+3}_center':f'dist_p{i}_center'}, inplace=True)
        
    # 'p{i}_[pos/vel]_[xyz]' <-> 'p{i+3}_[pos/vel]_[xyz]'
    for i in range(3):
        if pos_vel:
            for el in ['pos','vel']:
                for x in 'xyz':
                    t.rename(columns={f'p{i}_{el}_{x}': f'p{i+3}_{el}_{x}', f'p{i+3}_{el}_{x}':f'p{i}_{el}_{x}'}, inplace=True)
        # 'p{i}_boost' <-> 'p{i+3}_boost' 
        t.rename(columns={f'p{i}_boost': f'p{i+3}_boost', f'p{i+3}_boost':f'p{i}_boost'}, inplace=True)
        # 'p{i}_pos_side' <->  'p{i+3}_pos_side'
        t.rename(columns={f'p{i}_pos_side': f'p{i+3}_pos_side', f'p{i+3}_pos_side':f'p{i}_pos_side'}, inplace=True)
        # 'dist_p{i}_base' <-> 'dist_p{i+3}_base'
        t.rename(columns={f'dist_p{i}_base': f'dist_p{i+3}_base', f'dist_p{i+3}_base':f'dist_p{i}_base'}, inplace=True)
        # 'dist_p{i}_ball' <-> 'dist_p{i+3}_ball'
        t.rename(columns={f'dist_p{i}_ball': f'dist_p{i+3}_ball', f'dist_p{i+3}_ball':f'dist_p{i}_ball'}, inplace=True)
    
    # 'sec_p{i}_pos_[xyz]' <-> 'sec_p{i+3}_pos_[xyz]'
    for i in range(3):
        for x in 'xyz':
            t.rename(columns={f'sec_p{i}_pos_{x}': f'sec_p{i+3}_pos_{x}', f'sec_p{i+3}_pos_{x}':f'sec_p{i}_pos_{x}'}, inplace=True)
    
    # 'team_A_scoring_within_10sec' <->'team_B_scoring_within_10sec'
    t.rename(columns={'team_A_scoring_within_10sec':'team_B_scoring_within_10sec', 
                      'team_B_scoring_within_10sec':'team_A_scoring_within_10sec'}, inplace=True)
    return t
In [ ]:
### Reflexion

t1 = reflexion_y_axis(df, pos_vel=True)
t2 = reflexion_x_axis(df, pos_vel=True)

df_refl = pd.concat([t1,t2,df], ignore_index=True)
del t1, t2, df
In [15]:
new_ord = lambda a, b: {f'p{a}_pos_x':f'p{b}_pos_x',
                        f'p{a}_pos_y':f'p{b}_pos_y',
                        f'p{a}_pos_z':f'p{b}_pos_z',
                        f'p{a}_vel_x':f'p{b}_vel_x',
                        f'p{a}_vel_y':f'p{b}_vel_y',
                        f'p{a}_vel_z':f'p{b}_vel_z',
                        f'p{a}_boost':f'p{b}_boost',
                        f'dist_p{a}_base':f'dist_p{b}_base',
                        f'p{a}_pos_side':f'p{b}_pos_side',
                        f'dist_p{a}_ball':f'dist_p{b}_ball',
                        f'sec_p{a}_pos_x':f'sec_p{b}_pos_x',
                        f'sec_p{a}_pos_y':f'sec_p{b}_pos_y',
                        f'sec_p{a}_pos_z':f'sec_p{b}_pos_z',
                        f'p{a}_vel':f'p{b}_vel', 
                        f'p{a}_dist':f'p{b}_dist'}

new_mod = lambda a, b: {f'p{a}_boost':f'p{b}_boost',
                        f'dist_p{a}_base':f'dist_p{b}_base',
                        f'p{a}_pos_side':f'p{b}_pos_side',
                        f'dist_p{a}_ball':f'dist_p{b}_ball',
                        f'sec_p{a}_pos_x':f'sec_p{b}_pos_x',
                        f'sec_p{a}_pos_y':f'sec_p{b}_pos_y',
                        f'sec_p{a}_pos_z':f'sec_p{b}_pos_z',
                        f'p{a}_vel':f'p{b}_vel', 
                        f'dist_p{a}_center':f'dist_p{b}_center'}


# mantain core permutation only [0,1,2], [3,4,5]
per_teamA = [[0,2,1], [1,0,2], [1,2,0], [2,1,0], [2,0,1]]
per_teamB = [[3,5,4], [4,3,5], [4,5,3], [5,4,3], [5,3,4]]

def aux_func(temp_l, c):
    c0 = temp_l.index(c[0])
    c1 = temp_l.index(c[1])
    temp_l[c0],temp_l[c1] = temp_l[c1],temp_l[c0]
    return temp_l

def permutation_player(df, pos_vel=True):
    #n_ord = new_ord if pos_vel else new_mod
    n_ord = new_ord
    
    T = pd.DataFrame()
    for na,p_ta in enumerate(per_teamA):
        for nb,p_tb in enumerate(per_teamB):
            base_a = [0,1,2] ; base_b=[3,4,5]
            t = df.copy()
            for i in range(3):
                cols_ta, cols_tb = {},{}
                s_ta = (i, p_ta[i]) ; s_tb = (i+3, p_tb[i])
                if (i != p_ta[i]) and (base_a != p_ta):
                    base_a = aux_func(base_a, s_ta)
                    cols_ta = {**n_ord(i, p_ta[i]),**n_ord(p_ta[i], i)}
                    print('change',base_a)

                if (i+3 != p_tb[i]) and (base_b != p_tb):
                    base_b = aux_func(base_b, s_tb)
                    cols_tb = {**n_ord(i+3, p_tb[i]),**n_ord(p_tb[i], i+3)}
                    print('change',base_b)
                
                cols = {**cols_ta, **cols_tb}
                t.rename(columns=cols, inplace=True)
            
            cols_ta, cols_tb = {},{}
            if (base_a != p_ta):
                base_a = aux_func(base_a, (base_a[0], base_a[-1]) )
                cols_ta = {**n_ord(base_a[0], base_a[-1]),**n_ord(base_a[-1], base_a[0])}
                print('Last change: ', base_a)
               
            if (base_b != p_tb):
                base_b = aux_func(base_b, (base_b[0], base_b[-1]) )
                cols_tb = {**n_ord(base_b[0], base_b[-1]),**n_ord(base_b[-1], base_b[0])}
                print('Last change: ', base_b)
            
            cols = {**cols_ta, **cols_tb}
            t.rename(columns=cols, inplace=True)
            
            print((na,nb), base_a, base_b)
            print((na,nb), p_ta, p_tb, '\n')
            
            T = pd.concat([T,t],ignore_index=True)
            del t
    return T
In [ ]:
### Player Permutation
temp_df = df_refl[(df_refl['team_A_scoring_within_10sec']==1)|(df_refl['team_B_scoring_within_10sec']==1)]

df_per = permutation_player(temp_df[::6], pos_vel=False)
In [ ]:
df = pd.concat([df_refl, df_per], ignore_index=True)
del temp_df, df_refl, df_per

df['team_Null'] = (~((df['team_A_scoring_within_10sec']==1) | (df['team_B_scoring_within_10sec']==1))).astype('int8')
df['target_enc'] = (df['team_A_scoring_within_10sec'] + 2* df['team_B_scoring_within_10sec']).astype('int8')

Run the model¶

In [16]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
In [4]:
df = pd.read_hdf('../input/tbsoct2022-pro-data/train_0.25_v2.hdf', key='df', mode='r')

cols = ['bal_vel_xy','gamma_1', 'sigma_1',
        'p0_pos_side','p1_pos_side','p2_pos_side','p3_pos_side','p4_pos_side','p5_pos_side','ball_pos_side',
        'dist_p0_base', 'dist_p1_base', 'dist_p2_base',
        'dist_p3_base', 'dist_p4_base', 'dist_p5_base',
        'sec_p0_pos_x', 'sec_p0_pos_y', 'sec_p0_pos_z', 
        'sec_p1_pos_x', 'sec_p1_pos_y', 'sec_p1_pos_z', 
        'sec_p2_pos_x', 'sec_p2_pos_y', 'sec_p2_pos_z',
        'sec_p3_pos_x', 'sec_p3_pos_y', 'sec_p3_pos_z',
        'sec_p4_pos_x', 'sec_p4_pos_y', 'sec_p4_pos_z',
        'sec_p5_pos_x', 'sec_p5_pos_y', 'sec_p5_pos_z',
        'sec_ball_pos_x', 'sec_ball_pos_y', 'sec_ball_pos_z',
        'dist_side0','dist_side1']

temp_df.drop(cols, inplace=True, axis=1)

new_features(temp_df)
new_features2(temp_df)
In [21]:
## better aproach
col_var3 = ['ball_pos_x','ball_pos_y','ball_pos_z', 'ball_vel_x', 'ball_vel_y', 'ball_vel_z',    
            'p0_pos_x','p0_pos_y', 'p0_pos_z', 'p0_vel_x','p0_vel_y','p0_vel_z',
            'p1_pos_x','p1_pos_y', 'p1_pos_z', 'p1_vel_x','p1_vel_y','p1_vel_z',
            'p2_pos_x','p2_pos_y', 'p2_pos_z', 'p2_vel_x','p2_vel_y','p2_vel_z',
            'p3_pos_x','p3_pos_y', 'p3_pos_z', 'p3_vel_x','p3_vel_y','p3_vel_z',
            'p4_pos_x','p4_pos_y', 'p4_pos_z', 'p4_vel_x','p4_vel_y','p4_vel_z',
            'p5_pos_x','p5_pos_y', 'p5_pos_z', 'p5_vel_x','p5_vel_y','p5_vel_z',
            'p0_boost', 'p1_boost', 'p2_boost', 'p3_boost', 'p4_boost', 'p5_boost',
            'boost0_timer', 'boost1_timer', 'boost2_timer', 'boost3_timer','boost4_timer', 'boost5_timer', 
            'azimuth', 'elevation', 'ball_vel',
            'p0_vel', 'p1_vel', 'p2_vel', 'p3_vel', 'p4_vel', 'p5_vel', 
            'dist_ball',
            'dist_p0_ball','dist_p1_ball', 'dist_p2_ball', 'dist_p3_ball', 'dist_p4_ball','dist_p5_ball', 
            'dist_p0_center', 'dist_p1_center','dist_p2_center', 'dist_p3_center', 'dist_p4_center', 'dist_p5_center',
            'alpha_1', 'alpha_2', 'beta_1',  'beta_2',
            'sigma_2', 'gamma_2',
            'p0_alpha_1', 'p0_alpha_2','p0_beta_1','p0_beta_2',
            'p1_alpha_1', 'p1_alpha_2','p1_beta_1','p1_beta_2',
            'p2_alpha_1', 'p2_alpha_2','p2_beta_1','p2_beta_2',
            'p3_alpha_1', 'p3_alpha_2','p3_beta_1','p3_beta_2',
            'p4_alpha_1', 'p4_alpha_2','p4_beta_1','p4_beta_2',
            'p5_alpha_1', 'p5_alpha_2','p5_beta_1','p5_beta_2',
            'dif_p0','dif_p1','dif_p2','dif_p3','dif_p4','dif_p5',
            'mdist_p0', 'mdist_p1', 'mdist_p2','mdist_p3', 'mdist_p4', 'mdist_p5']
In [32]:
from sklearn.model_selection import GroupShuffleSplit
import numpy as np

group = temp_df['event_id'] # game_num', 'event_id'
y_k   = temp_df['target_enc']
y     = temp_df[['team_Null','team_A_scoring_within_10sec','team_B_scoring_within_10sec']]
X     = temp_df[col_var3]

#del df

# change test_size consume more memory?
splitter = GroupShuffleSplit(test_size=.05, n_splits=2, random_state = 7)
split = splitter.split(X,y, groups=group)
train_idx, val_idx = next(split)
  • Set train: 0.95 and test: 0.05
In [17]:
import tensorflow as tf
import tensorflow_addons as tfa
from tensorflow.keras import layers
# all 121
def simple_model(X_train, y_train, X_val, y_val, ep = 2, bs=1024, lr=0.0001): #, lambd = 0.001):
    model = tf.keras.Sequential([layers.Dense(256, input_shape=[118], activation=tfa.activations.mish),
                                 
                                 layers.BatchNormalization(),
                                 layers.Dropout(rate=0.3),
                                 layers.Dense(512, activation=tfa.activations.mish), 
                                 
                                 layers.BatchNormalization(),
                                 layers.Dense(128, activation=tfa.activations.mish),

                                 layers.Dense(3, activation='softmax')])
    
    optimizer = tf.keras.optimizers.Adam(learning_rate=lr)
    model.compile(optimizer=optimizer, loss='categorical_crossentropy',metrics=['accuracy'])
    
    callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                                min_delta=0.001, patience=5,
                                                restore_best_weights=True )
    history = model.fit(X_train,y_train,
                        validation_data=(X_val, y_val),
                        batch_size=bs, epochs=ep, callbacks=[callback])
    
    return model, history
In [34]:
from sklearn.preprocessing import StandardScaler
from sklearn.compose import make_column_transformer
from tensorflow.keras import mixed_precision


preprocessor = make_column_transformer( (StandardScaler(), col_var3) )

tf.keras.backend.clear_session()

X_tr = X.iloc[train_idx] ; y_tr = y.iloc[train_idx]
X_vl = X.iloc[val_idx]   ; y_vl = y.iloc[val_idx]

X_tr = preprocessor.fit_transform(X_tr)
X_vl = preprocessor.transform(X_vl)

model, history = simple_model(X_tr, y_tr, X_vl, y_vl, ep=30, bs=1024, lr=0.0001*7)#, lambd = 0.001)
Epoch 1/30
3232/3232 [==============================] - 129s 40ms/step - loss: 0.5667 - accuracy: 0.7592 - val_loss: 0.4779 - val_accuracy: 0.8010
Epoch 2/30
3232/3232 [==============================] - 125s 39ms/step - loss: 0.4734 - accuracy: 0.8037 - val_loss: 0.4604 - val_accuracy: 0.8087
Epoch 3/30
3232/3232 [==============================] - 122s 38ms/step - loss: 0.4591 - accuracy: 0.8095 - val_loss: 0.4532 - val_accuracy: 0.8109
Epoch 4/30
3232/3232 [==============================] - 122s 38ms/step - loss: 0.4515 - accuracy: 0.8128 - val_loss: 0.4475 - val_accuracy: 0.8143
Epoch 5/30
3232/3232 [==============================] - 127s 39ms/step - loss: 0.4470 - accuracy: 0.8145 - val_loss: 0.4428 - val_accuracy: 0.8150
Epoch 6/30
3232/3232 [==============================] - 128s 40ms/step - loss: 0.4435 - accuracy: 0.8163 - val_loss: 0.4424 - val_accuracy: 0.8162
Epoch 7/30
3232/3232 [==============================] - 139s 43ms/step - loss: 0.4410 - accuracy: 0.8173 - val_loss: 0.4404 - val_accuracy: 0.8169
Epoch 8/30
3232/3232 [==============================] - 127s 39ms/step - loss: 0.4390 - accuracy: 0.8183 - val_loss: 0.4391 - val_accuracy: 0.8179
Epoch 9/30
3232/3232 [==============================] - 135s 42ms/step - loss: 0.4373 - accuracy: 0.8190 - val_loss: 0.4374 - val_accuracy: 0.8190
Epoch 10/30
3232/3232 [==============================] - 142s 44ms/step - loss: 0.4358 - accuracy: 0.8196 - val_loss: 0.4367 - val_accuracy: 0.8189
Epoch 11/30
3232/3232 [==============================] - 129s 40ms/step - loss: 0.4346 - accuracy: 0.8201 - val_loss: 0.4367 - val_accuracy: 0.8179
Epoch 12/30
3232/3232 [==============================] - 124s 38ms/step - loss: 0.4333 - accuracy: 0.8208 - val_loss: 0.4358 - val_accuracy: 0.8192
Epoch 13/30
3232/3232 [==============================] - 122s 38ms/step - loss: 0.4321 - accuracy: 0.8212 - val_loss: 0.4359 - val_accuracy: 0.8191
Epoch 14/30
3232/3232 [==============================] - 119s 37ms/step - loss: 0.4316 - accuracy: 0.8215 - val_loss: 0.4360 - val_accuracy: 0.8188
Epoch 15/30
3232/3232 [==============================] - 119s 37ms/step - loss: 0.4304 - accuracy: 0.8220 - val_loss: 0.4346 - val_accuracy: 0.8198
Epoch 16/30
3232/3232 [==============================] - 122s 38ms/step - loss: 0.4297 - accuracy: 0.8224 - val_loss: 0.4349 - val_accuracy: 0.8194
Epoch 17/30
3232/3232 [==============================] - 119s 37ms/step - loss: 0.4289 - accuracy: 0.8227 - val_loss: 0.4338 - val_accuracy: 0.8199
Epoch 18/30
3232/3232 [==============================] - 118s 36ms/step - loss: 0.4279 - accuracy: 0.8231 - val_loss: 0.4339 - val_accuracy: 0.8201
Epoch 19/30
3232/3232 [==============================] - 121s 38ms/step - loss: 0.4274 - accuracy: 0.8235 - val_loss: 0.4335 - val_accuracy: 0.8204
Epoch 20/30
3232/3232 [==============================] - 121s 37ms/step - loss: 0.4265 - accuracy: 0.8239 - val_loss: 0.4338 - val_accuracy: 0.8206
Epoch 21/30
3232/3232 [==============================] - 118s 37ms/step - loss: 0.4260 - accuracy: 0.8241 - val_loss: 0.4337 - val_accuracy: 0.8208
Epoch 22/30
3232/3232 [==============================] - 123s 38ms/step - loss: 0.4252 - accuracy: 0.8244 - val_loss: 0.4349 - val_accuracy: 0.8200
Epoch 23/30
3232/3232 [==============================] - 121s 38ms/step - loss: 0.4247 - accuracy: 0.8248 - val_loss: 0.4339 - val_accuracy: 0.8202
Epoch 24/30
3232/3232 [==============================] - 121s 37ms/step - loss: 0.4240 - accuracy: 0.8250 - val_loss: 0.4331 - val_accuracy: 0.8203
In [35]:
t = pd.DataFrame(history.history)

f,ax=plt.subplots(ncols=2, figsize=(15,4))

t[['loss','val_loss']].plot(ax=ax[0])
t[['accuracy','val_accuracy']].plot(ax=ax[1])
Out[35]:
<AxesSubplot:>
In [19]:
pred = model.predict(X_vl)

Evalutation Metrics¶

In [20]:
from sklearn.metrics import accuracy_score, roc_auc_score

print('\n',accuracy_score(y_k.iloc[val_idx], np.argmax(pred, axis=1)))
print(roc_auc_score(y_vl, pred))
 0.8216992872496889
0.9313930444049507
In [21]:
from sklearn.metrics import mean_absolute_error

ind1 =  y_vl['team_A_scoring_within_10sec']==1 #.iloc[:,0]
ind2 =  y_vl['team_B_scoring_within_10sec']==1
ind3 =  y_vl['team_Null']==1

df_temp = pd.DataFrame(pred)

print('Team A winners:  ',mean_absolute_error(y_vl.loc[ind1,'team_A_scoring_within_10sec'], df_temp.loc[ind1.values,1]))
print('Team B winners:  ',mean_absolute_error(y_vl.loc[ind2,'team_B_scoring_within_10sec'], df_temp.loc[ind2.values,2]))
print('Team Null     :  ',mean_absolute_error(y_vl.loc[ind3,'team_Null'], df_temp.loc[ind3.values,0]))
Team A winners:   0.31683043
Team B winners:   0.33541942
Team Null     :   0.107504554
  • The Model can predict better no scoring situations.

Analyzing Bad Predictions¶

In [ ]:
# Team A scoring
temp = X.iloc[val_idx].loc[ind1].iloc[(df_temp.loc[ind1.values,1]<0.5).values]
In [ ]:
temp['ball_pos_y'].hist(bins=200)
  • The model can fail to predict target beacuse the ball is in the opponent Team field.
  • The model can fail to predict at kick-off. Players direction can help to predict strategies.
In [ ]:
temp['ball_pos_z'].hist(bins=200)
  • The great uncertainty at predictions is where ball is close to the field.

Check Permutation Importance of Features¶

Permutation importance is a technique that use column permutations, it determines how much this changes can affect the quality of the predictions.

The worse the prediction, the higher the importance of that specific feature.

Permutation Importance over snapshots were Team A is scoring.¶

In [19]:
from sklearn.metrics import mean_absolute_error

#define function to swap columns
def swap_columns(df, col1, col2):
    col_list = list(df.columns)
    x, y = col_list.index(col1), col_list.index(col2)
    col_list[y], col_list[x] = col_list[x], col_list[y]
    return df[col_list]


def check_Permutation_Importance(model, val_y_data, val_X_data):
    #n = len(val_y_data)
    #t_val_y  = val_y_data.values.reshape(-1,1)
    # cache of previous permutations
    res = {}; ans = {}
    
    for cnt, c in enumerate(val_X_data.columns):
        print(cnt)
        base = 0; total = 0
        for p in val_X_data.columns:
            cond1 = res.get(f'({c},{p})') is None
            cond2 = res.get(f'({p},{c})') is None
            if (cond1 and cond2):
                temp = swap_columns(val_X_data, c, p)
                #temp = preprocessor.transform(temp)
                # mean squared error
                #r = np.sum((t_val_y - model.predict(temp))**2)/n
                r = mean_absolute_error(val_y_data, model.predict(temp))
                if (c==p): base = r
                # save cache
                res[f"({c},{p})"] = r
            else:
                if res.get(f"({c},{p})") is None:
                    r = res[f"({p},{c})"]
                else:
                    r = res[f"({c},{p})"]
            # evaluate change in mse
            total += r - base
        ans[c] = total
    
    eval_df = pd.DataFrame(data={'Feature':list(ans.keys()), 'Importance':list(ans.values())})
    eval_df['Importance'] = eval_df['Importance']/sum(ans.values())
    eval_df.sort_values(by='Importance', inplace=True)
    return eval_df
In [ ]:
# Dependencie on 'TeamA' scoring

from sklearn.utils.random import sample_without_replacement

ind1 =  y_vl['team_A_scoring_within_10sec']==1

y_temp = y_vl.loc[ind1].iloc[:1000]
X_temp = pd.DataFrame(preprocessor.transform(X.iloc[val_idx].loc[ind1].iloc[:1000]), columns= col_var3)

eval_df = check_Permutation_Importance(model, y_temp, X_temp)
In [35]:
eval_df.set_index('Feature').plot(kind='barh', figsize=(10,30))
Out[35]:
<AxesSubplot:ylabel='Feature'>

The most important features are:

  • Ball positioning angles: alpha & beta
  • Minimum players distance to the ball 1 sec into the future as much (mdist_p[i])
  • Diference between velocity vectors (diff_p[i])

The worst features are:

  • Position and Velocity features, maybe because the most valuable information that can offer are refined on the new features implemented.

Submission¶

In [22]:
dtypes = {'index':'int32','game_num': 'int32', 'event_id': 'int32', 'event_time': 'float16',
          'ball_pos_x': 'float16', 'ball_pos_y': 'float16', 'ball_pos_z': 'float16',
          'ball_vel_x': 'float16', 'ball_vel_y': 'float16', 'ball_vel_z': 'float16',
          'p0_pos_x': 'float16', 'p0_pos_y': 'float16', 'p0_pos_z': 'float16',
          'p0_vel_x': 'float16', 'p0_vel_y': 'float16', 'p0_vel_z': 'float16', 'p0_boost': 'float16',
          'p1_pos_x': 'float16', 'p1_pos_y': 'float16', 'p1_pos_z': 'float16', 
          'p1_vel_x': 'float16', 'p1_vel_y': 'float16', 'p1_vel_z': 'float16', 'p1_boost': 'float16',
          'p2_pos_x': 'float16', 'p2_pos_y': 'float16', 'p2_pos_z': 'float16', 
          'p2_vel_x': 'float16', 'p2_vel_y': 'float16', 'p2_vel_z': 'float16', 'p2_boost': 'float16',
          'p3_pos_x': 'float16', 'p3_pos_y': 'float16', 'p3_pos_z': 'float16', 
          'p3_vel_x': 'float16', 'p3_vel_y': 'float16', 'p3_vel_z': 'float16', 'p3_boost': 'float16',
          'p4_pos_x': 'float16', 'p4_pos_y': 'float16', 'p4_pos_z': 'float16', 
          'p4_vel_x': 'float16', 'p4_vel_y': 'float16', 'p4_vel_z': 'float16', 'p4_boost': 'float16',
          'p5_pos_x': 'float16', 'p5_pos_y': 'float16', 'p5_pos_z': 'float16', 
          'p5_vel_x': 'float16', 'p5_vel_y': 'float16', 'p5_vel_z': 'float16', 'p5_boost': 'float16',
          'boost0_timer': 'float16', 'boost1_timer': 'float16', 'boost2_timer': 'float16', 
          'boost3_timer': 'float16', 'boost4_timer': 'float16', 'boost5_timer': 'float16', 
          'player_scoring_next': 'int8', 'team_scoring_next': 'object', 
          'team_A_scoring_within_10sec': 'int8', 'team_B_scoring_within_10sec': 'int8'}
In [23]:
test_df = pd.read_csv('../input/tabular-playground-series-oct-2022/test.csv', dtype=dtypes)

feature_eng(test_df)
new_features(test_df)
new_features2(test_df)
In [29]:
X_eval = test_df[col_var3]
X_eval = preprocessor.transform(X_eval)

pred = model.predict(X_eval)

test_df['team_null'] = pred[:,0]
test_df['team_A_scoring_within_10sec'] = pred[:,1]
test_df['team_B_scoring_within_10sec'] = pred[:,2]
In [31]:
final = test_df[['id','team_A_scoring_within_10sec','team_B_scoring_within_10sec']].copy()
final
Out[31]:
id team_A_scoring_within_10sec team_B_scoring_within_10sec
0 0 0.023890 0.027807
1 1 0.003067 0.230918
2 2 0.019368 0.089581
3 3 0.060639 0.021571
4 4 0.013900 0.037073
... ... ... ...
701138 701138 0.183625 0.026933
701139 701139 0.014722 0.065710
701140 701140 0.077823 0.065097
701141 701141 0.010819 0.107058
701142 701142 0.050167 0.028904

701143 rows × 3 columns

In [32]:
final.to_csv('sub.csv', index=False)

Future Steps:¶

A good approach for future development can be based on the analysis on a more refined trajectory prediction, i.e. the implementation of ball/player gravity.