kaggleの雑記

kaggleのNFLコンペから学ぶ、ポジションの欠損値の埋め方【事例】

特徴量生成のコードを読み解く、その6

https://www.kaggle.com/enzoamp/nfl-lightgbm

このNotebookのコードを解読していきます。コードの作成者はLorenzo Ampilさんです。

static_features

def static_features(df):
    static_features = df[df['NflId'] == df['NflIdRusher']][['GameId','PlayId','X','Y','S','A','Dis','Orientation','Dir',
                                                        'YardLine','Quarter','Down','Distance','DefendersInTheBox']].drop_duplicates()
    static_features['DefendersInTheBox'] = static_features['DefendersInTheBox'].fillna(np.mean(static_features['DefendersInTheBox']))

    return static_features

統計特徴量と言う関数で、Rusherに相当するレコードだけを抜粋してから、重複を削除し、DefendersInTheBoxには欠損値を平均値で埋めています。

personnel_features

one hotエンコーディングをしている様子?(ポジションのカウントを返しているので、厳密には少し違うかもしれない)

def personnel_features(df):
    personnel = df[['GameId','PlayId','OffensePersonnel','DefensePersonnel']].drop_duplicates()
    personnel['DefensePersonnel'] = personnel['DefensePersonnel'].apply(lambda x: split_personnel(x))
    personnel['DefensePersonnel'] = personnel['DefensePersonnel'].apply(lambda x: defense_formation(x))
    personnel['num_DL'] = personnel['DefensePersonnel'].apply(lambda x: x[0])
    personnel['num_LB'] = personnel['DefensePersonnel'].apply(lambda x: x[1])
    personnel['num_DB'] = personnel['DefensePersonnel'].apply(lambda x: x[2])

    personnel['OffensePersonnel'] = personnel['OffensePersonnel'].apply(lambda x: split_personnel(x))
    personnel['OffensePersonnel'] = personnel['OffensePersonnel'].apply(lambda x: offense_formation(x))
    personnel['num_QB'] = personnel['OffensePersonnel'].apply(lambda x: x[0])
    personnel['num_RB'] = personnel['OffensePersonnel'].apply(lambda x: x[1])
    personnel['num_WR'] = personnel['OffensePersonnel'].apply(lambda x: x[2])
    personnel['num_TE'] = personnel['OffensePersonnel'].apply(lambda x: x[3])
    personnel['num_OL'] = personnel['OffensePersonnel'].apply(lambda x: x[4])

    # Let's create some features to specify if the OL is covered
    personnel['OL_diff'] = personnel['num_OL'] - personnel['num_DL']
    personnel['OL_TE_diff'] = (personnel['num_OL'] + personnel['num_TE']) - personnel['num_DL']
    # Let's create a feature to specify if the defense is preventing the run
    # Let's just assume 7 or more DL and LB is run prevention
    personnel['run_def'] = (personnel['num_DL'] + personnel['num_LB'] > 6).astype(int)

    personnel.drop(['OffensePersonnel','DefensePersonnel'], axis=1, inplace=True)
    
    return personnel

1行目

personalのデータフレームに入れるときにゲームIDとプレイIDと2つのカラムを持ってきています。

  • OffensePersonnel – offensive team positional grouping
  • DefensePersonnel – defensive team positional grouping

2行目

def split_personnel(s):
    splits = s.split(',')
    for i in range(len(splits)):
        splits[i] = splits[i].strip()

    return splits

この関数の役割は文字列を分割して空文字を削除することです。下はこの関数の挙動を確認するための例です。

s="1 RB, 2 TE, 1 WR"
splits = s.split(',')
print(splits)
for i in range(len(splits)):
    splits[i] = splits[i].strip()
splits

このコードのアウトプットは以下のようになります。

['1 RB', '2 TE', '1 WR']

3行目

def defense_formation(l):
    dl = 0
    lb = 0
    db = 0
    other = 0

    for position in l:
        sub_string = position.split(' ')
        if sub_string == 'DL':
            dl += int(sub_string[0])
        elif sub_string in ['LB','OL']:
            lb += int(sub_string[0])
        else:
            db += int(sub_string[0])

    counts = (dl,lb,db,other)

    return counts

2行目のリストに変換したポジションのリストを分解し、数を計測してタプルで返している。これを

personnel['DefensePersonnel'] = personnel['DefensePersonnel'].apply(lambda x: defense_formation(x))

と言う風に変換することで、文字列は完全に数値だけのタプルになる。

(otherはどうなっっても0になるように見えるが、後から使うのだろうか?)

4, 5, 6行目

DefensePersonnelに入っている値がタプルのままだと学習に使えないので、それを改めて

  • num_DL
  • num_LB
  • num_DB

というデータにして入れ直している

7〜13行目

2〜6行目でやっていることをoffenseに対して実行しただけ。

ただしoffense_formation関数だけはちょっと特殊。

def offense_formation(l):
    qb = 0
    rb = 0
    wr = 0
    te = 0
    ol = 0

    sub_total = 0
    qb_listed = False
    for position in l:
        sub_string = position.split(' ')
        pos = sub_string
        cnt = int(sub_string[0])

        if pos == 'QB':
            qb += cnt
            sub_total += cnt
            qb_listed = True
        # Assuming LB is a line backer lined up as full back
        elif pos in ['RB','LB']:
            rb += cnt
            sub_total += cnt
        # Assuming DB is a defensive back and lined up as WR
        elif pos in ['WR','DB']:
            wr += cnt
            sub_total += cnt
        elif pos == 'TE':
            te += cnt
            sub_total += cnt
        # Assuming DL is a defensive lineman lined up as an additional line man
        else:
            ol += cnt
            sub_total += cnt
    # If not all 11 players were noted at given positions we need to make some assumptions
    # I will assume if a QB is not listed then there was 1 QB on the play
    # If a QB is listed then I'm going to assume the rest of the positions are at OL
    # This might be flawed but it looks like RB, TE and WR are always listed in the personnel
    if sub_total < 11:
        diff = 11 - sub_total
        if not qb_listed:
            qb += 1
            diff -= 1
        ol += diff

    counts = (qb,rb,wr,te,ol)

    return counts

この中の

if sub_total < 11

のあたりなのですが、11人のプレイヤー全員のポジションが特定できないことがあるようです。その時は仮定の元で入れてあげる処理をしています。(QBがいなかったらQBを入れています。QBがいるのに特定できていないかったらOLを入れているようです。)

OLはオフェンスラインだそうです。

https://second-effort.com/positions/offense/

14〜16行目

上で求めたポジションごとの数から、新しい特徴量を生成しています。

  • OL_diff(OL – オフェンスラインとDL – ディフェンスラインの人数差)
  • OL_TE_diff(「OL – オフェンスライン+TE- タイトライン」と「DL – ディフェンスライン」の人数差)
  • run_def(DL – ディフェンスラインとLB – ラインバッカーの合計が7以上なら1、そうでないなら0)

最後のrun_defを求める時は、一度booleanにしてからastypeで01に戻しています。

17行目

必要な特徴量の生成が終わったので、’OffensePersonnel’,’DefensePersonnel’をデータから落としています。

combine_features

def combine_features(relative_to_back, defense, static, personnel, deploy=deploy):
    df = pd.merge(relative_to_back,defense,on=['GameId','PlayId'],how='inner')
    df = pd.merge(df,static,on=['GameId','PlayId'],how='inner')
    df = pd.merge(df,personnel,on=['GameId','PlayId'],how='inner')

    if not deploy:
        df = pd.merge(df, outcomes, on=['GameId','PlayId'], how='inner')

    return df

最後に今で作ってきた特徴量を全て、ゲームIDとプレイIDをキーにして内部結合します。

deployはどういう意図で作成したのか読み取れませんでした。Yardsを結合するかどうかを選択できるようになっているようです。

kaggleのコンペから方向と距離についての特徴量生成を学ぶ特徴量生成のコードを読み解く、その1 https://www.kaggle.com/enzoamp/nfl-lightgbm こ...
kaggleのNFLコンペから、選手の行動・方向に関する特徴量生成を学ぶ特徴量生成のコードを読み解く、その2 https://www.kaggle.com/enzoamp/nfl-lightgbm こ...
kaggleのNFLコンペから、ランナーとその他の選手の切り分けた特徴量生成を学ぶ特徴量生成のコードを読み解く、その3 https://www.kaggle.com/enzoamp/nfl-lightgbm こ...
kaggleからpandasで縦のデータから統計データを取得する方法を学ぶ特徴量生成のコードを読み解く、その4 https://www.kaggle.com/enzoamp/nfl-lightgbm こ...
【kaggle】NFL Big Data Bowl のlightgbmを利用したコードの特徴量生成を読み解く、その5特徴量生成のコードを読み解く、その5 https://www.kaggle.com/enzoamp/nfl-lightgbm こ...
ABOUT ME
hirayuki
今年で社会人3年目になります。 日々体当たりで仕事を覚えています。 テーマはIT・教育です。 少しでも技術に親しんでもらえるよう、noteで4コマ漫画も書いています。 https://note.mu/hirayuki