Exploratory Data Analysis

In [1]:
import pandas as pd
import numpy as np
from sklearn import preprocessing
import gc, sys
gc.enable()

import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

pd.set_option('display.max_columns', 500)

import warnings
warnings.filterwarnings('ignore')
In [2]:
# настройка внешнего вида графиков в seaborn
sns.set_style("dark")
sns.set_palette("RdBu")
sns.set_context("notebook", font_scale = 1.5, rc = { "figure.figsize" : (15, 5), "axes.titlesize" : 18 })
In [3]:
# Memory saving function credit to https://www.kaggle.com/gemartin/load-data-reduce-memory-usage
def reduce_mem_usage(df):
    start_mem = df.memory_usage().sum() / 1024**2
    print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))

    for col in df.columns:
        col_type = df[col].dtype

        if col_type != object:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)  
            else:
                if c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)

    end_mem = df.memory_usage().sum() / 1024**2
    print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
    print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))

    return df

Загрузим данные и посмотрим с чем мы имеем дело

In [4]:
debug = False
In [5]:
if debug:
    df_train = reduce_mem_usage(pd.read_csv('../input/train_V2.csv', nrows=20000))
    df_test = reduce_mem_usage(pd.read_csv('../input/test_V2.csv', nrows=20000))
else:
    df_train = reduce_mem_usage(pd.read_csv('../input/train_V2.csv'))
    df_test = reduce_mem_usage(pd.read_csv('../input/test_V2.csv'))
Memory usage of dataframe is 983.90 MB
Memory usage after optimization is: 339.28 MB
Decreased by 65.5%
Memory usage of dataframe is 413.18 MB
Memory usage after optimization is: 140.19 MB
Decreased by 66.1%
In [6]:
display(df_train.head())
print(df_train.shape)
Id groupId matchId assists boosts damageDealt DBNOs headshotKills heals killPlace killPoints kills killStreaks longestKill matchDuration matchType maxPlace numGroups rankPoints revives rideDistance roadKills swimDistance teamKills vehicleDestroys walkDistance weaponsAcquired winPoints winPlacePerc
0 7f96b2f878858a 4d4b580de459be a10357fd1a4a91 0 0 0.000000 0 0 0 60 1241 0 0 0.000000 1306 squad-fpp 28 26 -1 0 0.0000 0 0.00 0 0 244.800003 1 1466 0.4444
1 eef90569b9d03c 684d5656442f9e aeb375fc57110c 0 0 91.470001 0 0 0 57 0 0 0 0.000000 1777 squad-fpp 26 25 1484 0 0.0045 0 11.04 0 0 1434.000000 5 0 0.6400
2 1eaf90ac73de72 6a4a42c3245a74 110163d8bb94ae 1 0 68.000000 0 0 0 47 0 0 0 0.000000 1318 duo 50 47 1491 0 0.0000 0 0.00 0 0 161.800003 2 0 0.7755
3 4616d365dd2853 a930a9c79cd721 f1f1f4ef412d7e 0 0 32.900002 0 0 0 75 0 0 0 0.000000 1436 squad-fpp 31 30 1408 0 0.0000 0 0.00 0 0 202.699997 3 0 0.1667
4 315c96c26c9aac de04010b3458dd 6dc8ff871e21e6 0 0 100.000000 0 0 0 45 0 1 1 58.529999 1424 solo-fpp 97 95 1560 0 0.0000 0 0.00 0 0 49.750000 2 0 0.1875
(4446966, 29)
In [7]:
df_train.columns
Out[7]:
Index(['Id', 'groupId', 'matchId', 'assists', 'boosts', 'damageDealt', 'DBNOs',
       'headshotKills', 'heals', 'killPlace', 'killPoints', 'kills',
       'killStreaks', 'longestKill', 'matchDuration', 'matchType', 'maxPlace',
       'numGroups', 'rankPoints', 'revives', 'rideDistance', 'roadKills',
       'swimDistance', 'teamKills', 'vehicleDestroys', 'walkDistance',
       'weaponsAcquired', 'winPoints', 'winPlacePerc'],
      dtype='object')
In [8]:
df_train.describe().T
Out[8]:
count mean std min 25% 50% 75% max
assists 4446966.0 0.233815 0.588573 0.0 0.000000 0.000000 0.000000 22.0
boosts 4446966.0 1.106908 1.715794 0.0 0.000000 0.000000 2.000000 33.0
damageDealt 4446966.0 130.633118 169.886948 0.0 0.000000 84.239998 186.000000 6616.0
DBNOs 4446966.0 0.657876 1.145743 0.0 0.000000 0.000000 1.000000 53.0
headshotKills 4446966.0 0.226820 0.602155 0.0 0.000000 0.000000 0.000000 64.0
heals 4446966.0 1.370147 2.679982 0.0 0.000000 0.000000 2.000000 80.0
killPlace 4446966.0 47.599350 27.462937 1.0 24.000000 47.000000 71.000000 101.0
killPoints 4446966.0 505.006042 627.504896 0.0 0.000000 0.000000 1172.000000 2170.0
kills 4446966.0 0.924783 1.558445 0.0 0.000000 0.000000 1.000000 72.0
killStreaks 4446966.0 0.543955 0.710972 0.0 0.000000 0.000000 1.000000 20.0
longestKill 4446966.0 22.993483 51.476089 0.0 0.000000 0.000000 21.320000 1094.0
matchDuration 4446966.0 1579.506440 258.739856 9.0 1367.000000 1438.000000 1851.000000 2237.0
maxPlace 4446966.0 44.504670 23.828105 1.0 28.000000 30.000000 49.000000 100.0
numGroups 4446966.0 43.007593 23.289495 1.0 27.000000 30.000000 47.000000 100.0
rankPoints 4446966.0 892.010457 736.647779 -1.0 -1.000000 1443.000000 1500.000000 5910.0
revives 4446966.0 0.164659 0.472167 0.0 0.000000 0.000000 0.000000 39.0
rideDistance 4446966.0 606.092346 1496.470459 0.0 0.000000 0.000000 0.190975 40710.0
roadKills 4446966.0 0.003496 0.073373 0.0 0.000000 0.000000 0.000000 18.0
swimDistance 4446966.0 4.509240 30.237843 0.0 0.000000 0.000000 0.000000 3823.0
teamKills 4446966.0 0.023868 0.167394 0.0 0.000000 0.000000 0.000000 12.0
vehicleDestroys 4446966.0 0.007918 0.092612 0.0 0.000000 0.000000 0.000000 5.0
walkDistance 4446966.0 1148.516968 1180.552734 0.0 155.100006 685.599976 1976.000000 25780.0
weaponsAcquired 4446966.0 3.660488 2.456544 0.0 2.000000 3.000000 5.000000 236.0
winPoints 4446966.0 606.460131 739.700444 0.0 0.000000 0.000000 1495.000000 2013.0
winPlacePerc 4446965.0 0.472814 0.306804 0.0 0.200000 0.458300 0.740700 1.0

Любой предварительный анализ начинается с поиска пропусков в данных. Для этого воспользуемся замечательной функцией missing_values_table:

In [9]:
# Function to calculate missing values by column
def missing_values_table(df):
        # Total missing values
        mis_val = df.isnull().sum()
        
        # Percentage of missing values
        mis_val_percent = 100 * df.isnull().sum() / len(df)
        
        # Make a table with the results
        mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
        
        # Rename the columns
        mis_val_table_ren_columns = mis_val_table.rename(
        columns = {0 : 'Missing Values', 1 : '% of Total Values'})
        
        # Sort the table by percentage of missing descending
        mis_val_table_ren_columns = mis_val_table_ren_columns[
            mis_val_table_ren_columns.iloc[:,1] != 0].sort_values(
        '% of Total Values', ascending=False).round(1)
        
        # Print some summary information
        print ("Your selected dataframe has " + str(df.shape[1]) + " columns.\n"      
            "There are " + str(mis_val_table_ren_columns.shape[0]) +
              " columns that have missing values.")
        
        # Return the dataframe with missing information
        return mis_val_table_ren_columns

В тренировочном наборе есть матч с одним игроком, у которого отсутствует целевая переменная:

In [10]:
missing_values_table(df_train)
Your selected dataframe has 29 columns.
There are 1 columns that have missing values.
Out[10]:
Missing Values % of Total Values
winPlacePerc 1 0.0
In [11]:
df_train[df_train['winPlacePerc'].isnull()]
Out[11]:
Id groupId matchId assists boosts damageDealt DBNOs headshotKills heals killPlace killPoints kills killStreaks longestKill matchDuration matchType maxPlace numGroups rankPoints revives rideDistance roadKills swimDistance teamKills vehicleDestroys walkDistance weaponsAcquired winPoints winPlacePerc
2744604 f70c74418bb064 12dfbede33f92b 224a123c53e008 0 0 0.0 0 0 0 1 0 0 0 0.0 9 solo-fpp 1 1 1574 0 0.0 0 0.0 0 0 0.0 0 0 NaN

Так как у нас всего одно пропущенное значение, смело удаляем его:

In [12]:
df_train.drop(df_train[df_train['winPlacePerc'].isnull()].index.values, inplace=True)

В тестовом наборе пропусков нет, хорошо.

In [13]:
missing_values_table(df_test)
Your selected dataframe has 28 columns.
There are 0 columns that have missing values.
Out[13]:
Missing Values % of Total Values

Посмотрим на распределение целевого признака winPlacePerc. Видим, что он имеет более/менее равномерное распределение:

In [14]:
print(df_train['winPlacePerc'].describe())
plt.figure(figsize=(15, 5))
sns.distplot(df_train['winPlacePerc'], color='g', bins=100, hist_kws={'alpha': 0.4});
count    4.446965e+06
mean     4.728141e-01
std      3.068041e-01
min      0.000000e+00
25%      2.000000e-01
50%      4.583000e-01
75%      7.407000e-01
max      1.000000e+00
Name: winPlacePerc, dtype: float64

Для начала посмотрим на распределение количественных признаков:

In [15]:
list(set(df_train.dtypes.tolist()))
Out[15]:
[dtype('float32'), dtype('int8'), dtype('int16'), dtype('O')]
In [16]:
df_num = df_train.select_dtypes(include = ['float32', 'int16', 'int8'])
df_num.head()
Out[16]:
assists boosts damageDealt DBNOs headshotKills heals killPlace killPoints kills killStreaks longestKill matchDuration maxPlace numGroups rankPoints revives rideDistance roadKills swimDistance teamKills vehicleDestroys walkDistance weaponsAcquired winPoints winPlacePerc
0 0 0 0.000000 0 0 0 60 1241 0 0 0.000000 1306 28 26 -1 0 0.0000 0 0.00 0 0 244.800003 1 1466 0.4444
1 0 0 91.470001 0 0 0 57 0 0 0 0.000000 1777 26 25 1484 0 0.0045 0 11.04 0 0 1434.000000 5 0 0.6400
2 1 0 68.000000 0 0 0 47 0 0 0 0.000000 1318 50 47 1491 0 0.0000 0 0.00 0 0 161.800003 2 0 0.7755
3 0 0 32.900002 0 0 0 75 0 0 0 0.000000 1436 31 30 1408 0 0.0000 0 0.00 0 0 202.699997 3 0 0.1667
4 0 0 100.000000 0 0 0 45 0 1 1 58.529999 1424 97 95 1560 0 0.0000 0 0.00 0 0 49.750000 2 0 0.1875
In [17]:
df_num.hist(figsize=(16, 20), bins=50, xlabelsize=10, ylabelsize=8);

Всего в игре есть три основных вида матча (matchType):

  1. solo (каждый сам за себя)
  2. duo (игра с одним напарником)
  3. и squad (игра в комманде от 2 до 4 человек в каждой)

Каждый из трёх видов делится по особенностям игры (fpp - игра от первого лица). Каждый тип матча имеет свои плюсы, например, при игре от третьего лица можно заглядывать за угол из-за укрытия, а fpp режим будет привычен для монстров CS. Интересно посмотреть, как тип матча влияет на вероятность победы.

Рассмотрим распределение целевого признака в рамках основных типов матчей:

In [18]:
cat_vars = df_train.select_dtypes(include = ['O'])
cat_vars['matchType'].value_counts()
Out[18]:
squad-fpp           1756186
duo-fpp              996691
squad                626526
solo-fpp             536761
duo                  313591
solo                 181943
normal-squad-fpp      17174
crashfpp               6287
normal-duo-fpp         5489
flaretpp               2505
normal-solo-fpp        1682
flarefpp                718
normal-squad            516
crashtpp                371
normal-solo             326
normal-duo              199
Name: matchType, dtype: int64

Объединим похожие типы:

In [19]:
solo = cat_vars.loc[cat_vars['matchType'].isin(['solo', 'solo-fpp', 'normal-solo', 'normal-solo-fpp'])]['matchType'].value_counts()
solo = list(solo.index)

duo = cat_vars.loc[cat_vars['matchType'].isin(['duo', 'duo-fpp', 'normal-duo', 'normal-duo-fpp'])]['matchType'].value_counts()
duo = list(duo.index)

squad = cat_vars.loc[cat_vars['matchType'].isin(['squad', 'squad-fpp', 'normal-squad', 'normal-squad-fpp'])]['matchType'].value_counts()
squad = list(squad.index)

crash = cat_vars.loc[cat_vars['matchType'].isin(['crashfpp', 'crashtpp', 'flaretpp', 'flarefpp'])]['matchType'].value_counts()
crash = list(crash.index)
In [20]:
plt.figure(figsize=(15, 5))
sns.set(color_codes=True)

for b_type in solo:
    
    subset = df_train[df_train['matchType'] == b_type]

    sns.kdeplot(subset['winPlacePerc'],
               label = b_type, shade = False, alpha = 0.8);
    
# label the plot
plt.xlabel('winPlacePerc', size = 20); plt.ylabel('Density', size = 20); 
plt.title('Density Plot of winPlacePerc by matchType', size = 28);
In [21]:
plt.figure(figsize=(15, 5))
sns.set(color_codes=True)

for b_type in duo:
    
    subset = df_train[df_train['matchType'] == b_type]

    sns.kdeplot(subset['winPlacePerc'],
               label = b_type, shade = False, alpha = 0.8);
    
# label the plot
plt.xlabel('winPlacePerc', size = 20); plt.ylabel('Density', size = 20); 
plt.title('Density Plot of winPlacePerc by matchType', size = 28);
In [22]:
plt.figure(figsize=(15, 5))
sns.set(color_codes=True)

for b_type in squad:
    
    subset = df_train[df_train['matchType'] == b_type]

    sns.kdeplot(subset['winPlacePerc'],
               label = b_type, shade = False, alpha = 0.8);
    
# label the plot
plt.xlabel('winPlacePerc', size = 20); plt.ylabel('Density', size = 20); 
plt.title('Density Plot of winPlacePerc by matchType', size = 28);
In [23]:
plt.figure(figsize=(15, 5))
sns.set(color_codes=True)

for b_type in crash:
    
    subset = df_train[df_train['matchType'] == b_type]

    sns.kdeplot(subset['winPlacePerc'],
               label = b_type, shade = False, alpha = 0.8);
    
# label the plot
plt.xlabel('winPlacePerc', size = 20); plt.ylabel('Density', size = 20); 
plt.title('Density Plot of winPlacePerc by matchType', size = 28);

Видно, что обычный режим и fpp имеют схожую плотность распределения во всех трёх основных типах матчей. Возможно, объединение похожих по распределению типов игры положительно скажется на предсказании победителя (как показала практика - нет).

В любом случае, объединим их для уменьшения количества категориальных признаков:

In [24]:
mapper = {'solo-fpp':'solo', 'normal-solo-fpp':'normal-solo', 'normal-duo-fpp':'normal-duo', 'squad-fpp':'squad', \
          'normal-squad-fpp':'normal-squad', 'crashfpp':'crash', 'crashtpp':'crash', 'flaretpp':'flare', 'flarefpp':'flare'}
        
df_train['matchType'].replace(mapper, inplace=True)
In [25]:
df_train['matchType'].value_counts()
Out[25]:
squad           2382712
duo-fpp          996691
solo             718704
duo              313591
normal-squad      17690
crash              6658
normal-duo         5688
flare              3223
normal-solo        2008
Name: matchType, dtype: int64

Теперь посмотрим на корреляцию количественных признаков с целевым. Это даст нам предварительное представление о важности признаков.

In [26]:
corr = df_num.corrwith(df_num['winPlacePerc'])[:-1].reset_index()
corr.columns = ['Index', 'Correlations']
corr = corr.set_index('Index')
corr = corr.sort_values(by=['Correlations'], ascending = False)
plt.figure(figsize=(4,15))
fig = sns.heatmap(corr, annot=True, fmt="g", cmap='YlGnBu')
plt.title("Correlation of Variables with winPlacePerc");

Получаем список признаков с наиболее сильной корреляцией:

In [27]:
high_corr_features_list = corr[abs(corr['Correlations']) > 0.5].sort_values(by=['Correlations'], ascending=False)
print("There is {} strongly correlated values with winPlacePerc:\n{}".format(len(high_corr_features_list), high_corr_features_list))
There is 4 strongly correlated values with winPlacePerc:
                 Correlations
Index                        
walkDistance         0.810888
boosts               0.634234
weaponsAcquired      0.583806
killPlace           -0.719069

Рассмотрим более детально взаимосвязь наших признаков с вероятностью победы:

In [28]:
from tqdm import tqdm

%time
for i in tqdm(range(0, len(df_num.columns), 5)):
    sns.pairplot(data=df_num,
                x_vars=df_num.columns[i:i+5],
                y_vars=['winPlacePerc'])
  0%|          | 0/5 [00:00<?, ?it/s]
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 6.2 µs
100%|██████████| 5/5 [04:57<00:00, 59.38s/it]