3位解法、考えたこと

対象銘柄を株と投資信託・ファンドの2グループに分け、株式市場と債券市場全体の値動きから個別銘柄の値動きを予測しました
[推定対象についての認識]
  • 推定対象の銘柄には普通の「株」と投資信託・ファンド(以降、ファンド類)が混在している
  • ファンド類には債券のみを投資対象にしたものが相当数ある(注1)
[株価(ファンド価格)予測についての主観的な前提]
  • 大まかには、株の個別銘柄は株式市場の値動きにある程度は連動するはず
  • ファンド類の値動きは株式市場の値動きと債券市場の値動きである程度は説明できるはず。ただし、それぞれの市場の値動きへの連動性は銘柄によって異なる
[方針]
  1. 株は"VTHR"、債券は"IEF"を市場全体の値動きのベンチマークにする(いずれもこのコンペの対象銘柄)
  2. 推定対象の銘柄を株とファンド類に分ける。分類の基準はcompany_list.csvでのSector定義の有無
  3. 株・ファンド類とも各週の対数収益率を目的変数としてモデル化する
  4. 直接対数収益率を予測するのはVTHR、IEFのみで、他の銘柄の対数収益率はこれらの予測値をインプットとして予測する
  5. 株は各銘柄の銘柄をプールして全銘柄共通のLightGBMのモデルにする(注2)。ただし、期間中1度でも株価が1ドルを下回ったことのある銘柄はtrainの対象から外す(注3)
  6. ファンド類は銘柄ごとのLinearRegressionのモデルにする

注1 他にも、REIT(不動産投資信託)やNameからは商品性が判断できないファンド等も結構ある。また、よく見るとSectorが定義されている銘柄の中にもこのような銘柄がある(キリがないので今回は無視)
注2 今思うと、LightGBMよりも、過学習しないような手法に持ち込んだ方がスッキリする
注3 株価があまりに低い銘柄は、市場全体の値動きとは無関係に、極端な値動きをしがちなため

[特記事項]

コンペ開催中に提出したファイルを作成したコードに誤りがあり、本来意図していなかった処理をしていました。誤りがあった個所は株の特徴量作成の部分です。
提出したファイル public:0.03756, private:0.03701
意図した通りに処理した結果 public:0.03774, private:0.03706

# Googleドライブ直下に必要ファイルを圧縮したProbSpace_USEquity.zipがある前提です
from google.colab import drive
drive.mount('/content/drive')
!unzip /content/drive/MyDrive/ProbSpace_USEquity.zip
Mounted at /content/drive
Archive:  /content/drive/MyDrive/ProbSpace_USEquity.zip
  inflating: company_list.csv        
  inflating: submission_template.csv  
  inflating: train_data.csv          
import warnings
warnings.simplefilter('ignore')

import datetime
from IPython.display import clear_output

import pandas as pd
import numpy as np

from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

from lightgbm import LGBMRegressor

!pip install --q prophet
from prophet import Prophet

clear_output()

print('Done!')
Done!
train = pd.read_csv('train_data.csv')
train['date'] = pd.to_datetime(train['Date'])
train = train.set_index('date')
train.drop('Date', axis=1, inplace=True)

company = pd.read_csv('company_list.csv')
company = company[company['Symbol'].isin(train.columns)].drop('List', axis=1)
company = company.drop_duplicates().set_index('Symbol')

# "株"のSymbolのリスト
stocks = company[pd.isna(company['Sector'])==False].index

# 株以外のSymbolのリスト(Companyに記載のない銘柄はfundの可能性があるためこちらに分類)
funds = [c for c in train.columns if c not in stocks]
ln_train = np.log(train)
ln_return = ln_train.diff().dropna(how='all')

funds_to_est = [f for f in funds if f not in ['VTHR', 'IEF']] #間接的に予測するファンド類
stock_returns = ln_return[stocks]
fund_returns = ln_return[funds_to_est]
equity_bm = ln_return['VTHR']
bond_bm = ln_return['IEF']
print(f"株式市場全体のベンチマーク - VTHR: {company.at['VTHR', 'Name']}")
print(f"債券市場全体のベンチマーク - IEF: {company.at['IEF', 'Name']}")
株式市場全体のベンチマーク - VTHR: Vanguard Russell 3000 ETF
債券市場全体のベンチマーク - IEF: iShares 7-10 Year Treasury Bond ETF
# VTHR, IEFの対数収益率はProphetで予測
def predict_bm(asset:str)->float:
    if asset=='equity':
        y, reg = 'VTHR', 'IEF'
    elif asset=='bond':
        y, reg = 'IEF', 'VTHR'

    bms = ln_return[[y, reg]].reset_index()
    bms['lag_reg'] = bms[reg].shift()
    bms = bms.rename(columns={'date':'ds', y:'y'}).drop(reg, axis=1).dropna(how='any')

    m = Prophet()
    m.add_regressor('lag_reg')
    m.fit(bms)
    future = m.make_future_dataframe(periods=1, freq='W')
    future = future.merge(ln_return[reg].reset_index(drop=True), how='left', left_index=True, right_index=True)
    future.rename(columns={reg:'lag_reg'}, inplace=True)
    forecast = m.predict(future)

    return forecast.iloc[417]['yhat']

return_equity_bm = predict_bm('equity')
return_bond_bm = predict_bm('bond')
clear_output()
print(f"最終週のベンチマークリターン予測値 株(VTHR):{return_equity_bm:.3%}, 債券(IEF):{return_bond_bm:.3%}")
最終週のベンチマークリターン予測値 株(VTHR):0.654%, 債券(IEF):0.139%
last_day = datetime.datetime(2019,11,17)

# 最終週の対数収益率を格納する辞書
test_return_dic = {}
test_return_dic['VTHR'] = return_equity_bm
test_return_dic['IEF'] = return_bond_bm

特徴量は

  • 同時点のVTHRの収益率
  • 前週のVTHRとの相対収益率:順張り的な投資行動を反映
  • Sector:Sectorにより市場との連動性が違いそう
  • IPOyear:特に欠損値に意味がありそう(古参の企業)
# 株価が1ドル未満になったことのある銘柄をtrainから外す
tmp = (ln_train<0).sum()
not_too_cheap = tmp[tmp==0].index
train_sample = [s for s in not_too_cheap if s in stocks]

sector_dic = dict(zip(company['Sector'].unique(), range(company['Sector'].nunique()+1)))
company['n_Sector'] = company['Sector'].map(sector_dic)
company_dic = dict(company)

lag_return_df = pd.concat([stock_returns.shift(), equity_bm.shift()], axis=1)

# 提出ファイルでは以下の2行の処理をしていなかったため、本来は前週のVTHRとの相対収益率のはずが、単なる前週の収益率になっていた
'''
for s in stocks:
    lag_return_df[s] = lag_return_df[s] - lag_return_df['VTHR']
'''

df = pd.DataFrame()

for s in train_sample:
    tmp_df = pd.concat([ln_return[[s, 'VTHR']].rename(columns={s:'return', 'VTHR':'stock_mkt'}),
                        lag_return_df[s]], axis=1).rename(columns={s:'rel_to_mkt'})
    tmp_df['Symbol'] = s
    tmp_df['IPOyear'] = company_dic['IPOyear'][s]
    tmp_df['Sector'] = company_dic['n_Sector'][s]
    df = pd.concat([df, tmp_df[1:]])

df.reset_index(inplace=True)

test_df = pd.DataFrame(ln_return[-1:][stocks].T-ln_return.at[last_day, 'VTHR']).rename(columns={last_day:'rel_to_mkt'})
test_df['stock_mkt'] = return_equity_bm
test_df['IPOyear'] = company_dic['IPOyear'][test_df.index]
test_df['Sector'] = company_dic['n_Sector'][test_df.index]
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
oof = np.zeros(len(df))
pred = np.zeros(len(stocks))
lgb = LGBMRegressor(max_depth=5, subsample=0.8, random_state=0)

for i, (train_idx, valid_idx) in enumerate(skf.split(df, df['date'])):
    x_train, y_train = df.loc[train_idx][['stock_mkt', 'rel_to_mkt', 'IPOyear', 'Sector']], df.loc[train_idx]['return']
    x_valid, y_valid = df.loc[valid_idx][['stock_mkt', 'rel_to_mkt', 'IPOyear', 'Sector']], df.loc[valid_idx]['return']
    lgb.fit(x_train, y_train, categorical_feature=['Sector'])
    oof[valid_idx] = lgb.predict(x_valid)
    oof_rmse = np.sqrt(mean_squared_error(y_valid, oof[valid_idx]))
    print(f"{oof_rmse:.4f}", end=' ')
    pred += lgb.predict(test_df[['stock_mkt', 'rel_to_mkt', 'IPOyear', 'Sector']])

tot_rmse = np.sqrt(mean_squared_error(df['return'], oof))
print()
print(f"Total RMSE:{tot_rmse:.4f}")

for i, s in enumerate(stocks):
    test_return_dic[s] = pred[i]
0.0507 0.0510 0.0502 0.0508 0.0505 
Total RMSE:0.0506
ファンド類

特徴量は

  • 同時点のVTHRの収益率
  • 同時点のIEFの収益率

回帰手法の変更や特徴量の追加・変更をするつもりがなかったのでCVもしていません

bm_forecast = np.array([return_equity_bm, return_bond_bm]).reshape(1,-1)

for f in funds_to_est:
    tmp_df = ln_return[[f, 'VTHR', 'IEF']].rename(columns={'VTHR':'stock_mkt', 'IEF':'bond_mkt'})
    lr = LinearRegression()
    lr.fit(tmp_df[['stock_mkt', 'bond_mkt']], tmp_df[f])
    test_return_dic[f] = lr.predict(bm_forecast)[0]
投稿ファイルの作成
price_dic = {}

for c in ln_train.columns:
    price_dic[c] = np.exp(ln_train.at[last_day, c] + test_return_dic[c])

sub = pd.read_csv('submission_template.csv').set_index('id')

for i in sub.index:
    sub.at[i, 'y'] = price_dic[i]

sub.to_csv('submission.csv')

添付データ

  • PeobSpace_USEquity_solution.ipynb?X-Amz-Expires=10800&X-Amz-Date=20241004T184313Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIP7GCBGMWPMZ42PQ
  • Favicon
    new user
    コメントするには 新規登録 もしくは ログイン が必要です。