kaggleの雑記

Ion Switchingのkaggleコンペのカーネルを読み込む-その1

コンペについて

https://www.kaggle.com/c/liverpool-ion-switching

リバプールの大学が行っている細胞研究です。観測器から取得した電気信号のデータを元にして細胞のチャンネルが開放されている瞬間を検知することが目標です。

今回読み込むカーネル

AlexFocusさんの[ION SWITCHING] LGB + CatB Modelを読み込もうと思います。

データの読み込み

import numpy as np 
import pandas as pd
from sklearn import *
import lightgbm as lgb

train = pd.read_csv('/kaggle/input/liverpool-ion-switching/train.csv')
test = pd.read_csv('/kaggle/input/liverpool-ion-switching/test.csv')
train.shape, test.shape

単純なデータの読み込みです。今回のデータはテストと訓練に明確に分かれており、使いやすいですね。

特徴量の追加

def features(df):
    # 後記
    return df

train = features(train)
test = features(test)

特徴量を追加しています。それでは早速featureの中身を見ていきましょう。

まず加工対象のデータを見ます。trainは以下の様な形をしています。

時間と電気信号の大きさとその時にチャンネルが開放されているかどうかの3種類だけです。次は順にコードを追っていきます。

# time列を基準にソートし直す。(reset_index(drop=True)で列に入れない様にしてインデックスを外す。)
df = train.sort_values(by=['time']).reset_index(drop=True)
# indexをtimeを10000倍して1引いた数にする。(あまり変化はないが、読み込み時の動作不具合排除かと想定)
df.index = ((df.time * 10_000) - 1).values
# バッチを列として持つ。5,000,000行あるので、100バッチ
df['batch'] = df.index // 50_000
# バッチ単位ごとのindex。50000件のバッチが100個あるので、0~49,999のインデックスが100回振られる
df['batch_index'] = df.index  - (df.batch * 50_000)
# バッチ毎にスライス?している。50000行に5000行ごとに0~9の値を振っている
df['batch_slices'] = df['batch_index']  // 5_000
# indexとbacth_slicesを3桁長さの文字列に揃えて足し合わせた文字列

df['batch_slices2'] = df.apply(lambda r: '_'.join([str(r['batch']).zfill(3), str(r['batch_slices']).zfill(3)]), axis=1)

この処理をすることで、5万の周期(100分割のデータ)、5千の周期(100分割をさらに10分割したデータ)、そしてそれらを足し合わせて全体としての流れを持つデータができた。

5万レコード毎に、5千レコード毎にこれから統計量を取得します。

    # バッチ単位およびバッチスライス単位でカラムを追加する
    for c in ['batch','batch_slices2']:
        # 一時的に辞書型に格納
        d = {}
        d['mean'+c] = df.groupby([c])['signal'].mean()  # 平均
        d['median'+c] = df.groupby([c])['signal'].median()  # 中央値
        d['max'+c] = df.groupby([c])['signal'].max()  # 最大値
        d['min'+c] = df.groupby([c])['signal'].min()  # 最小値
        d['std'+c] = df.groupby([c])['signal'].std()  # 標準偏差
        # diffで次レコードとの差分を取り、その絶対値を取得して平均をとる。(バッチサイズ毎に)
        d['mean_abs_chg'+c] = df.groupby([c])['signal'].apply(lambda x: np.mean(np.abs(np.diff(x))))
        # 絶対値をとった後の最大値
        d['abs_max'+c] = df.groupby([c])['signal'].apply(lambda x: np.max(np.abs(x)))
        # 絶対値をとった後の最初うち
        d['abs_min'+c] = df.groupby([c])['signal'].apply(lambda x: np.min(np.abs(x)))
        # DataFrameに格納する
        for v in d:
            df[v] = df[c].map(d[v].to_dict())
        # 最大値と最小値の差分
        df['range'+c] = df['max'+c] - df['min'+c]
        # 最大値と最小値の割合
        df['maxtomin'+c] = df['max'+c] / df['min'+c]
        # 最大値と最小値の平均
        df['abs_avg'+c] = (df['abs_min'+c] + df['abs_max'+c]) / 2

取得するのは平均値・中央値・最大値・最小値・標準偏差・次レコードとの差分の絶対値の平均などなどです。

次レコードとの差分の平均値が見慣れない値ではありましたが、分散に近い機能を果たせそうな気はします。

    # 1レコード分足す様にシフトした値で、空行となってしまう1行目には0を置換
    df['signal_shift_+1'] = [0,] + list(df['signal'].values[:-1])
    # 1レコード分引く様にシフトした値で、空行となってしまう最終行には0を置換
    df['signal_shift_-1'] = list(df['signal'].values[1:]) + [0]
    # 1行目が空行となってしまう様に、batch_indexの最初の行には前のバッチのデータのが入り込んでしまう。
    # それをnanにしている。(0でもいい様な気がするけどどうなのだろうか)
    for i in df[df['batch_index']==0].index:
        df['signal_shift_+1'][i] = np.nan
    # 上の理由と同様
    for i in df[df['batch_index']==49999].index:
        df['signal_shift_-1'][i] = np.nan
    # batch_slicesに対しての処理は不要だろうか?・・・

    # 'time', 'signal', 'open_channels', 'batch', 'batch_index', 'batch_slices', 'batch_slices2'以外の列に対して
    # signalとの差分を取得している
    for c in [c1 for c1 in df.columns if c1 not in ['time', 'signal', 'open_channels', 'batch', 'batch_index', 'batch_slices', 'batch_slices2']]:
        df[c+'_msignal'] = df[c] - df['signal']

ここはなかなか理解が難しかったですし、全て理解できていません。特にbatch_indexに対してはshiftに対する丁寧な処理(はみ出したデータをnanとしたり)しているのですが、batch_slicesに対しては処理が行われていないので、気になるところです。

学習に向けてデータを整理

# 不必要なカラムをのぞいて、学習に必要なカラムを変数colに格納
col = [c for c in train.columns if c not in ['time', 'open_channels', 'batch', 'batch_index', 'batch_slices', 'batch_slices2']]
# 学習用のデータを分割
x1, x2, y1, y2 = model_selection.train_test_split(train[col], train['open_channels'], test_size=0.3, random_state=7)
# trainを消去(メモリ削減)
del train

ここはコードと挿入したコメントの通りです

LightGBMによる学習と予測

# カスタムメトリック関数
def lgb_Metric(preds, dtrain):
    """
    引数には予測結果と訓練データが入ってくる様にする
    """
    labels = dtrain.get_label()
    # 予測値を0~10の範囲に限定してINT型に変更
    preds = np.round(np.clip(preds, 0, 10)).astype(int)
    # https://en.wikipedia.org/wiki/Cohen%27s_kappa カッパ係数を利用
    score = metrics.cohen_kappa_score(labels, preds, weights = 'quadratic')
    return ('KaggleMetric', score, True)
 
# ハイパーパラメータを定義する
params = {'learning_rate': 0.8, 'max_depth': 7, 'num_leaves':2**7+1, 'metric': 'rmse', 'random_state': 7, 'n_jobs':-1} 
# 学習データをx1,y1 評価データをx2,y2として2000回まで学習、50回ごとに評価指標毎のスコアをprintして、50回以上スコアが上がらなければ停止する
model = lgb.train(params, lgb.Dataset(x1, y1), 2000,  lgb.Dataset(x2, y2), verbose_eval=50, early_stopping_rounds=50, feval=lgb_Metric)
# 予測値を出力
preds = model.predict(test[col], num_iteration=model.best_iteration)
# 予測値を0~10の間に限定する
test['open_channels'] = np.round(np.clip(preds, 0, 10)).astype(int)

# LightGBMの予測値として出力
test[['time','open_channels']].to_csv('submission_ligth.csv', index=False, float_format='%.4f')

カスタムメトリックを利用しています。カスタムメトリックについてはもみじあめさんの記事がわかりやすいです。

カスタムメトリックの評価指標はカッパ係数であり、計算方法はWikipediaにあります。

CatBoostによる学習と予測

from catboost import Pool,CatBoostRegressor

# Initialize CatBoostRegressor
model = CatBoostRegressor(task_type = "CPU",
                          iterations=1000,
                          learning_rate=0.1,
                          random_seed = 42,
                          depth=2,
                         )
# Fit model
model.fit(x1, y1)
# Get predictions
preds_catb = model.predict(test[col])
test['open_channels'] = np.round(np.clip(preds_catb, 0, 10)).astype(int)
test[['time','open_channels']].to_csv('submission_cat3.csv', index=False, float_format='%.4f')

特にコメントするポイントはなく、コードの通りです。

アンサンブル学習

preds_comb = 0.75 * preds + 0.25 * preds_catb
test['open_channels'] = np.round(np.clip(preds_comb, 0, 10)).astype(int)
test[['time','open_channels']].to_csv('submission_comb.csv', index=False, float_format='%.4f')

LightGBMの予測を0.75、Catboostの予測を0.25の割合でアンサンブルさせています。

参考

ABOUT ME
hirayuki
今年で社会人3年目になります。 日々体当たりで仕事を覚えています。 テーマはIT・教育です。 少しでも技術に親しんでもらえるよう、noteで4コマ漫画も書いています。 https://note.mu/hirayuki