プロKagglerブログ

プロKagglerがKaggleに参加して得た独自の知見

LGBM baseline (with テクニカル指標)

こんにちは、tera と申します。

私は現在、株式会社音圧爆上げくんという会社に所属しており、プロKagglerとして活動しています。 私は現在プロKagglerとして、業務の一環で、現在Kaggleで行われているコンペの一つである JPX Tokyo Stock Exchange Prediction に参加しています。

また、今回の記事は、Kaggle上にNotebookとして同じ内容の記事を英語で投稿しています。 リンクは こちら です。

はじめに

LGBM baselineは数多くのコードベースが公開されていますが、ラグ特徴量(テクニカル指標など)を使ったbaselineはまだ数が少ない状況です。 これには理由があり、kaggleの時系列APIの仕様で1銘柄1レコードずつしか一度に取得&評価できないため、testデータとtrainデータを上手くマージしながら管理しつつ毎度特徴量を計算し直す必要があり、調整に手間がかかるためです。

今回はその面倒なラグ特徴量(テクニカル指標)を使ったLBスコアを出す手順をまとめました。

talibのインストール

!pip install ../input/talib-source/talib_binary-0.4.19-cp37-cp37m-manylinux1_x86_64.whl
import talib as ta

talibはテクニカル指標を計算するpythonライブラリとして有名なものですが、インストールに工夫が必要です。 talib-sourceとして、whlファイルをデータセットに準備しているのでそちらを使ってインストールを行います。

テクニカル指標計算 関数

def add_technical(df):
    op = df['Open']
    hi = df['High']
    lo = df['Low']
    cl = df['Close']
    volume = df['Volume']
    hilo = (hi + lo) / 2

    # print('calc ta overlap')
    df['BBANDS_upperband'], df['BBANDS_middleband'], df['BBANDS_lowerband'] = ta.BBANDS(cl, timeperiod=5, nbdevup=2, nbdevdn=2, matype=0)
    df['BBANDS_upperband'] -= hilo
    df['BBANDS_middleband'] -= hilo
    df['BBANDS_lowerband'] -= hilo
    df['DEMA'] = ta.DEMA(cl, timeperiod=30) - hilo
    df['EMA'] = ta.EMA(cl, timeperiod=30) - hilo
    df['HT_TRENDLINE'] = ta.HT_TRENDLINE(cl) - hilo
    df['KAMA'] = ta.KAMA(cl, timeperiod=30) - hilo
    df['MA'] = ta.MA(cl, timeperiod=30, matype=0) - hilo
    df['MIDPOINT'] = ta.MIDPOINT(cl, timeperiod=14) - hilo
    df['SMA'] = ta.SMA(cl, timeperiod=30) - hilo
    df['T3'] = ta.T3(cl, timeperiod=5, vfactor=0) - hilo
    # df['TEMA'] = ta.TEMA(cl, timeperiod=30) - hilo
    df['TRIMA'] = ta.TRIMA(cl, timeperiod=30) - hilo
    df['WMA'] = ta.WMA(cl, timeperiod=30) - hilo

    # print('calc ta momentum')
    df['ADX'] = ta.ADX(hi, lo, cl, timeperiod=14)
    df['ADXR'] = ta.ADXR(hi, lo, cl, timeperiod=14)
    df['APO'] = ta.APO(cl, fastperiod=12, slowperiod=26, matype=0)
    df['AROON_aroondown'], df['AROON_aroonup'] = ta.AROON(hi, lo, timeperiod=14)
    df['AROONOSC'] = ta.AROONOSC(hi, lo, timeperiod=14)
    df['BOP'] = ta.BOP(op, hi, lo, cl)
    df['CCI'] = ta.CCI(hi, lo, cl, timeperiod=14)
    df['DX'] = ta.DX(hi, lo, cl, timeperiod=14)
    df['MACD_macd'], df['MACD_macdsignal'], df['MACD_macdhist'] = ta.MACD(cl, fastperiod=12, slowperiod=26, signalperiod=9)
    # skip MACDEXT MACDFIX
    df['MFI'] = ta.MFI(hi, lo, cl, volume, timeperiod=14)
    df['MINUS_DI'] = ta.MINUS_DI(hi, lo, cl, timeperiod=14)
    df['MINUS_DM'] = ta.MINUS_DM(hi, lo, timeperiod=14)
    df['MOM'] = ta.MOM(cl, timeperiod=10)
    df['PLUS_DI'] = ta.PLUS_DI(hi, lo, cl, timeperiod=14)
    df['PLUS_DM'] = ta.PLUS_DM(hi, lo, timeperiod=14)
    df['RSI'] = ta.RSI(cl, timeperiod=14)
    df['STOCH_slowk'], df['STOCH_slowd'] = ta.STOCH(hi, lo, cl, fastk_period=5, slowk_period=3, slowk_matype=0, slowd_period=3, slowd_matype=0)
    df['STOCHF_fastk'], df['STOCHF_fastd'] = ta.STOCHF(hi, lo, cl, fastk_period=5, fastd_period=3, fastd_matype=0)
    df['STOCHRSI_fastk'], df['STOCHRSI_fastd'] = ta.STOCHRSI(cl, timeperiod=14, fastk_period=5, fastd_period=3, fastd_matype=0)
    # df['TRIX'] = ta.TRIX(cl, timeperiod=30)
    df['ULTOSC'] = ta.ULTOSC(hi, lo, cl, timeperiod1=7, timeperiod2=14, timeperiod3=28)
    df['WILLR'] = ta.WILLR(hi, lo, cl, timeperiod=14)

    # print('calc ta volume')
    df['AD'] = ta.AD(hi, lo, cl, volume)
    df['ADOSC'] = ta.ADOSC(hi, lo, cl, volume, fastperiod=3, slowperiod=10)
    df['OBV'] = ta.OBV(cl, volume)

    # print('calc ta vola')
    df['ATR'] = ta.ATR(hi, lo, cl, timeperiod=14)
    df['NATR'] = ta.NATR(hi, lo, cl, timeperiod=14)
    df['TRANGE'] = ta.TRANGE(hi, lo, cl)

    # print('calc ta cycle')
    df['HT_DCPERIOD'] = ta.HT_DCPERIOD(cl)
    df['HT_DCPHASE'] = ta.HT_DCPHASE(cl)
    df['HT_PHASOR_inphase'], df['HT_PHASOR_quadrature'] = ta.HT_PHASOR(cl)
    df['HT_SINE_sine'], df['HT_SINE_leadsine'] = ta.HT_SINE(cl)
    df['HT_TRENDMODE'] = ta.HT_TRENDMODE(cl)

    # print('calc ta stats')
    df['BETA'] = ta.BETA(hi, lo, timeperiod=5)
    df['CORREL'] = ta.CORREL(hi, lo, timeperiod=30)
    df['LINEARREG'] = ta.LINEARREG(cl, timeperiod=14) - cl
    df['LINEARREG_ANGLE'] = ta.LINEARREG_ANGLE(cl, timeperiod=14)
    df['LINEARREG_INTERCEPT'] = ta.LINEARREG_INTERCEPT(cl, timeperiod=14) - cl
    df['LINEARREG_SLOPE'] = ta.LINEARREG_SLOPE(cl, timeperiod=14)
    df['STDDEV'] = ta.STDDEV(cl, timeperiod=5, nbdev=1)

    return df

talibで使える指標は概ね載せています。 LBGMの場合、不要な特徴量を追加してもスコアが劣化しにくいので、すべて載せてもある程度スコアには影響するはずです。

CSVロード

prices = pd.read_csv("../input/jpx-tokyo-stock-exchange-prediction/train_files/stock_prices.csv")

一旦trainの全データを使っていますが、スコアを伸ばす場合、supplemental_filesとマージして期間を限定すると良いと思います。

特徴量追加

prices.groupby('SecuritiesCode').apply(add_technical).dropna(axis=0)

groupbyを使ってSecuritiesCodeごとにテクニカル指標を計算します。

学習

model_o = LGBMRegressor(learning_rate=0.6818202991034834, max_bin=95, n_estimators=655, num_leaves=1263, random_seed=0)

ハイパーパラメタは他のnotebookのものを流用しています。 チューニングの余地はあります。

予測

# trainを初期状態としてセット
past_df = prices.copy()

# sample_prediction: 各銘柄1行ずつ
for i, (_prices, options, financials, trades, secondary_prices, sample_prediction) in enumerate(iter_test):
    current_date = _prices["Date"].iloc[0]
    print(f"current_date: {current_date}")

    # リークを防止するため、時系列APIから受け取ったデータより未来のデータを削除
    if i == 0:
        past_df = past_df.loc[past_df["Date"] < current_date]
    
    # リソース確保のため古い履歴を削除
    threshold = (pd.Timestamp(current_date) - pd.offsets.BDay(80)).strftime("%Y-%m-%d")
    print(f"threshold: {threshold}")
    past_df = past_df.loc[past_df["Date"] >= threshold]
    
    # _pricesの列調整
    _prices['Date'] = pd.to_datetime(_prices['Date'])
    _prices['DateInt'] = _prices['Date'].dt.strftime("%Y%m%d").astype(int)

    # 最新レコードと履歴をマージ
    past_df = pd.concat([past_df, _prices]).reset_index()

    # 履歴に対して特徴量再計算
    past_df = past_df.groupby('SecuritiesCode').apply(add_technical)

    # 最新レコード取り出し
    df = past_df.query(f'Date == "{current_date}"')

    # predict
    sample_prediction["Prediction"] = model_o.predict(df[fit_columns])

    # pred(リターン)を降順ソートしてランク付け
    sample_prediction = sample_prediction.sort_values(by="Prediction", ascending=False).drop_duplicates(subset=['SecuritiesCode'])
    sample_prediction.Rank = np.arange(0,2000)

    # コードで昇順ソート
    sample_prediction = sample_prediction.sort_values(by="SecuritiesCode", ascending=True)
    sample_prediction.drop(["Prediction"], axis=1)

    # 送信
    submission = sample_prediction[["Date","SecuritiesCode","Rank"]]
    env.predict(submission)

キモの部分ですが、past_dfの管理がややこしく、追加/削除/型変換/特徴量再計算/最新レコード分離 あたりがラグ特徴量を使うための処理になります。

お知らせ

最後に人材募集となりますが,株式会社音圧爆上げくんではプロKagglerを募集しています。
少しでも興味の有る方はぜひ以下のリンクをご覧の上ご応募ください。
Wantedlyリンク