G-Research Crypto Competition(timestamp,Targetの抜け(コインごと))

timestampが全通貨で抜けているtimestampがあることを確認したけど、そういえばコインごとにみたときはtimestampってどれだけ抜けているの?と思って確認。

全コインで抜けているtimestampの要因は出題者がデータまとめた際のエラーや取引所自体が落ちたとか?かなと。一方、あるコインのみ抜けているtimestampはそのコインが対象の1分間取引されなかったのではないかと思う。後者は動いてないだけだから前の時点の値で補完してもよい気がする。

コインごとにどれだけ抜けているか確認

コインによってデータ取得開始期間が違うため、取得開始以降で全体の何%がNaNであったかをCloseとTargetそれぞれについて確認。

id2name = details.set_index("Asset_ID")["Asset_Name"].to_dict()

adds = []
for k,v in id2name.items():
    
    tmp = data[data.Asset_ID == k].copy()

    full_time = pd.DataFrame(pd.to_datetime(full_timestamp)).rename(columns= {0: "org_timestamp"})
    tmp = pd.merge(full_time, tmp, left_on= "org_timestamp", right_on= "Time", how= "left")
    tmp.sort_values("org_timestamp", inplace= True)
    tmp["timestamp"] = tmp["timestamp"].fillna(method= "ffill")
    tmp.dropna(subset= ["timestamp"], inplace= True)
    first_time = tmp["org_timestamp"].iloc[0]

    agg_dict = {
        "org_timestamp": "count",
#         "Time": "first",
        "Close": "count",
        "Target": "count",
    }
    agg = tmp.agg(agg_dict)
    agg["Close%"] = 100. - (agg.Close / agg.org_timestamp) * 100.
    agg["Target%"] = 100. - (agg.Target / agg.org_timestamp) * 100.
    agg["first_time"] = first_time
    agg = pd.DataFrame(agg)
    agg["asset_name"] = v
    adds.append(agg)


tmp = pd.concat(adds, axis= 0, sort= True)\
    .reset_index()\
    .rename(columns= {0: "value"})
pivot = tmp.pivot(index= "asset_name", columns= "index", values= "value")

pivot = pd.merge(pivot, details.set_index("Asset_Name")["Weight"], left_index= True, right_index= True, how= "inner")
pivot[["Weight", "first_time", "Close%", "Target%"]].sort_values("Weight", ascending= False)

f:id:iiiiikamirin:20220115230617p:plain

当たり前だけど、CloseのNaN比率が低いとTargetのNaN比率も低い。CloseのNaN比率の低さはVolumeの変動の低さと相関してそう。ある程度のVolumeで安定していれば取引がつかないこともなくCloseがNaNとなることもなくなる的な。試しにコインごとのVolumeの標準偏差を出してみるとドージ・ステラは確かに大きい。Moneroは標準偏差は大きくないがそもそも平均がとても小さい。Volumeが関係している気が。。

agg_dict = {
    "Volume": ["mean","median","min", "max", "std"]
}
pivot = df_v.groupby("Asset_ID").agg(agg_dict)
pivot.columns= ["_".join(i) for i in pivot.columns]
pivot = pivot.rename(index= details.set_index("Asset_ID")["Asset_Name"].to_dict())
pivot = pd.merge(pivot, details.set_index("Asset_Name")["Weight"], left_index= True, right_index= True, how= "inner")
pivot[["Weight", "Volume_mean", "Volume_max", "Volume_std"]].sort_values("Weight", ascending= False)

f:id:iiiiikamirin:20220115235919p:plain

G-Research Crypto Competition(timestampの抜け)

Targetを作成しているnotebookをみつけたので、それをコピー。

ってか、Targetは加重平均に対する超過平均ではなくて、アルファみたいだな。

timestampに抜けがある

その中で気になったのが、trainデータに各月初日の最初の1分のデータがないという話。

要するに、毎月の最初の1分間が欠落しているので、その1分前と16分前のTargetsはリターンを計算するためのClose priceがないのでNaNになるはずなのに、それらの値が計算されているのです。つまり、これらの場合のTargetsは、私たちが見ることのできないデータに基づいて計算されているのか、それとも間違ったタイムスタンプに基づいて計算されているのでしょうか?

ほう。ということで「各月初日の最初の1分」以外にも抜けがあるのか含めて確認。まずは、そもそも何日間のデータがあるはずなのか確認。下の通り、1359日。

full_timestamp = crypto_df["timestamp"]\
    .sort_values()\
    .apply(lambda x: datetime.datetime.fromtimestamp(x))\
    .unique()

tmp = pd.DataFrame(pd.to_datetime(full_timestamp)).rename(columns= {0: "org_timestamp"})
dt_min = tmp["org_timestamp"].min()
dt_max = tmp["org_timestamp"].max()
dt_max - dt_min
Timedelta('1358 days 23:59:00')

今度はHHMM(時間:分)ごとに要素数をカウントして、やたら少ないHHMMがないか確認。雑だけど、たしかに00:00のところだけ要素数が少ない。

tmp["hhmm_timestamp"] = tmp["org_timestamp"].apply(lambda x: x.replace(year= 2000, month= 1, day= 1))
hhmm = tmp.groupby("hhmm_timestamp").count()

sns.lineplot(data= hhmm)
hhmm.head()

f:id:iiiiikamirin:20220108210138p:plain

value_counts()するとこんな感じ。00:00以外は1つ抜けがあるかないか。

hhmm["org_timestamp"].value_counts()

f:id:iiiiikamirin:20220108210850p:plain

00:00以外についてもなんか抜けがあっても1つだけというのは気持ち悪いな。ランダムなら2つ抜けるとかあってもよさそう。同じ日に連続して何時間か抜けたりしているのか??とりあえず以下の2通りの可能性を考えたけど、出題者が抜いている気が何となくする。前者ならすべて1つずつなんてきれいにならないだろうし、Bitcoinが1分間トレードされなかったという時間があるとはあんまり思えない。。

  • 全通貨取引その1分間取引がなかった

  • 出題者が意図的にデータ削除orデータ成形のミス

とりあえず、その抜けってある一日に集中しているのかそれとも分散しているのか確認。なぜかわからんが2019/10/16と2019/10/23の2日に抜けは集中している。あと毎月初日は1つ抜けているみたい(これが00:00か)。2019/10/16が抜けているのはなぜだ。。ビットコインの価格推移みてもそんなに動いた日ではないのに。。

tmp["yyyymmdd_timestamp"] = tmp["org_timestamp"].apply(lambda x: x.replace(hour= 0, minute= 0))
yyyymmdd = tmp.groupby("yyyymmdd_timestamp")["org_timestamp"].count()
yyyymmdd.value_counts()

f:id:iiiiikamirin:20220108213955p:plain

sns.lineplot(data= yyyymmdd.iloc[:-1])
yyyymmdd.sort_values().head(10)

f:id:iiiiikamirin:20220108214033p:plain

ってか、この2019/10/16あたりってTargetはあるのか?

自分のPCで作成した関数をKaggleのNotebookで使用する方法

Pycharmとかで作成した関数を、Kaggleのノートブック上で読み込んで使用できたら楽だなあ、と思って始めました。

もっといい方法があると思うけど、いまの自分にはこれが限界だったので、その方法をとりえず備忘録として。

  1. 自身で作成したコードをGithubにあげる。2.それをzip形式でダウンロードする。3.KaggleのDatasetにアップロードする。

  2. 自身で作成したコードをGithubにあげる。

参考サイト:

【超入門】初心者のためのGitとGitHubの使い方 - RAKUS Developers Blog | ラクス エンジニアブログ

一回設定してしまえば、あとは「1-6. ローカルリポジトリにコミットする」以降でリモートレポジトリにプッシュすればよさそう。

2.それをzip形式でダウンロードする。

Github上でレポジトリ開いて緑のCodeボタンおせばzip形式でダウンロードできる

3.KaggleのDatasetにアップロードする。

画像の通りUpload datasetをクリック

f:id:iiiiikamirin:20220108180554p:plain

適当な名前でさっきダウンロードしたzipファイルをドラッグ

f:id:iiiiikamirin:20220108180717p:plain

ちなみに、githubから直接できそうだったのになんか無理だった。。理由がわからない。。

f:id:iiiiikamirin:20220108180913p:plain

あとはNotebook上で以下の感じで読み込めばできた~!

data_folder = "../input/ttttest/Crypto-master/"
!ls $data_folder

# import module we'll need to import our custom module
from shutil import copyfile

# copy our file into the working directory (make sure it has .py suffix)
test = "../input/ttttest/Crypto-master/esty.py"
# copyfile(src = "../input/my_functions.py", dst = "../working/my_functions.py")
copyfile(src = test, dst = "../working/my_functions.py")

# import all our functions
# from my_functions import *
import my_functions as pytest

G-Research Crypto Forecasting(欠損値確認、コインごとの取引量)

やるといってもなにをしようかと。。。。EDAとかでデータ見せてくれてるものはあるけど、はあそうですか。。。って感じになってしまいがち。

予測するものは将来15分間の超過リターン(Target: Residual log-returns for the asset over a 15 minute horizon.)。具体的には下の画像。 f:id:iiiiikamirin:20211224153928p:plain

これ見る感じ、とりあえず各通貨ごとに重みづけした指数を作成しないと始まらないのでは??特徴量つくるにしても、終値の推移より指数対比の推移をみないと発見はないだろうなと思ったので、まずは指数を作ることに。さーっと共有されているNotebookみてみたけど、この指数作成しているっぽいものを見つけられなかったので自分で作ろうかと。

単純に「通貨のリターン」ー「重みづけ指数」みたいな特徴量作ったらそれなりにいい特徴量になるのでは??

  • 悩みポイント

  • timestampに抜けとかないのか??

df全体に関して欠損値を確認。vwap以外はなし。ただ、timestampがすべてそろっているかは別問題。

df = crypto_df.copy(deep= True)
df.columns = [str.lower(i) for i in df.columns]
df.isnull().sum(axis= 0)
timestamp         0
asset_id          0
count             0
open              0
high              0
low               0
close             0
volume            0
vwap              9
target       750338
dtype: int64

ではtimestampの抜けは?Dogeのように最近できて昔のデータがそもそもないやつは除いて確認してみたりした(na_mid列)けど、それでも結構抜け漏れあり。抜けもれあると1日変化と3日変化の区別がつかないと思うので、これはさすがに補完した方がよさそう。前の値とかで補完したら同じ話になるので、線形補完で。

pivot = df.pivot(index= "timestamp", columns= "asset_id", values= "close")
asset_id2name = asset_details.set_index("asset_id")["asset_name"].to_dict()

fillna = pivot.rename(columns= asset_id2name).fillna(method= "ffill").isnull().sum(axis= 0)
not_fillna = pivot.rename(columns= asset_id2name).isnull().sum(axis= 0)
fillna = pd.DataFrame(fillna).rename(columns= {0: "fillna"})
not_fillna = pd.DataFrame(not_fillna).rename(columns= {0: "not_fillna"})

na_check = pd.merge(fillna, not_fillna, left_index= True, right_index= True)
na_check["na_mid"] = na_check["not_fillna"] - na_check["fillna"]

f:id:iiiiikamirin:20211224161751p:plain

  • timestampの抜けを埋める。フルのtimestampを作成→それでreset_indexの方法で。

フルのtimestamp作成。すべて60(秒)間隔ではなかったけど、資産間でラグがあっていればいいかなと思ったのでよいことに。

full_timestamp = df.timestamp.unique()
pd.Series(np.diff(np.sort(full_timestamp))).value_counts()

f:id:iiiiikamirin:20211224162509p:plain

reset_indexかつ欠損値補完

・・・ここまでやって気づいたけど、欠損値ってもしやその1分でトレードがなかったときでは?????たしかに、夜中とかなくてもおかしくないか????

ということで、count(取引数)と一応Volume(取引量)の最小数を確認。せっかくなので最大と平均も。すると取引量最大のBitCoinでも最小は1ということは、欠損値は取引0=補完せずに欠損値であることに意味があるのでは。。線形補完は違うと思った。取引0=前の時点と変化なしなので終値などはfillnaでcountなどは0を入れればよい気が。

agg_dict = {
    "count": ["mean", "min", "max"],
    "volume": ["mean", "min", "max"],    
}

group = df.groupby("asset_id", as_index= False).agg(agg_dict)
group["asset_id"] = group["asset_id"].apply(lambda x: asset_id2name[x])
group.columns = ['_'.join(col) for col in group.columns]

f:id:iiiiikamirin:20211224164142p:plain

というか、このデータはへえ、って感じだったのでちょっとグラフ書いてみることに。取引数は中央値>平均値なのでやっぱり取引数の波は激しくあるタイミングでみんな集中して取引するみたいですな。大きく動いた後あとなのか同時なのか、下落なのか上昇なのか(下落っぽいけど)など深堀りの余地はありそう。特にDogeは中央値と平均値の差が激しい気がする。激しい=投機的コインのようなイメージ受けるけどな。実際に平均/中央値だしてみたらDogeが圧倒的。さすが柴犬。

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

# col = "count_mean"
col = [
    "count_mean",
    "count_median"    
]
show = group.set_index("asset_id_")\
    .loc[:, col]\
    .stack()\
    .reset_index()\
    .rename(columns= {"level_1": "hue", 0: "value"})

fig = plt.figure()
ax = fig.add_subplot()
# sns.barplot(data= group, x= "asset_id_", y= col, ax= ax)
sns.barplot(data= show, x= "asset_id_", y= "value", hue= "hue", ax= ax)
ax.tick_params(axis= "x",rotation = 270)
# ax.set_title(col)
ax.set_title(col[0].split("_")[0])

f:id:iiiiikamirin:20211224170229p:plain

平均/中央値

f:id:iiiiikamirin:20211224170722p:plain

平均/中央値(柴犬除き)

f:id:iiiiikamirin:20211224170841p:plain

あと1取引あたりのボリューム。大型のフローが入りやすいのか小型なのか。BitCoinは個人のすそ野広そうだし小型では??案の定Dogeあたりのマイナーコインのフローがでかい。まじでコインによって差がでた。

group["one_impact_mean"] = group.volume_mean / group.count_mean
group["one_impact_median"] = group.volume_median / group.count_median

col = [
    "one_impact_mean",
    "one_impact_median",
]
show = group.set_index("asset_id_")\
    .loc[:, col]\
    .stack()\
    .reset_index()\
    .rename(columns= {"level_1": "hue", 0: "value"})

fig = plt.figure()
ax = fig.add_subplot()
sns.barplot(data= show, x= "asset_id_", y= "value", hue= "hue", ax= ax)
ax.tick_params(axis= "x",rotation = 270)

f:id:iiiiikamirin:20211224171557p:plain

G-Research Crypto Forecasting(コードの提出方法)

ビットコインはちょっと気になるということで、これ!

予測の提出方法がよくわからない。。ファイルではなくコードを提出するのか??ということでとりあえず、コードの提出方法試したのでその備忘録。結論はファイル提出するときとそんなにかわんないっぽい。

  1. とりあえずコード作る。 今回は主催者が用意してるBasic Submission Templateをそのまま使用。右上Save Versionを押す。

f:id:iiiiikamirin:20211224151451p:plain

  1. ノートブックを閉じて、下の画面のSubmit Predictionで提出。

f:id:iiiiikamirin:20211224151737p:plain

  • 補足メモ

test_dfにtimestampやcloseなどが入っているので、row_idをキーにsample_prediction_dfに予測したTargetを入れればよさそう。ただ、test_dfの中身をみたらtimestampが1つdatetime.datetime(2021, 6, 13, 0, 3)しかない。これは実際にはどんどん増やされていくということ??提出後の実際の市場データをもとに評価するらしいので、おそらく変わっていくのでしょう。

f:id:iiiiikamirin:20211224152727p:plain

とりあえずコンペの紹介文を和訳。

暗号化予測チュートリアル G-Research暗号予測コンペティション G-Research Crypto forecasting competitionでは、参加者は主要な暗号通貨を束ねた価格リターンを予測する課題を持っています。このチュートリアルノートでは、参加者の皆様が参加しやすいように、暗号通貨予測に関連するいくつかのコンセプトについて説明します。 このノートブックでは、暗号予測への導入、データセットの構造と要素、関連する統計的特性、いくつかのMLベースラインモデルの構築、コード送信の例を説明しています。

暗号通貨市場 まず、暗号の世界について簡単に紹介します。暗号通貨は非常に人気のある不安定な市場となっており、投資家に大きなリターン(と同時に損失も)をもたらしています。ビットコイン(BTC)、イーサ(ETH)、ドージコイン(DOGE)など、多くの人が聞いたことのある主要な暗号通貨が数千種類も誕生しています。 暗号通貨は暗号取引所で広く取引されており、CryptoCompareによると、昨年1年間に毎日平均410億ドルが取引されています(2021年7月25日現在)。 異なる暗号通貨間の価格変動は、相互に大きく関連しています。例えば、ビットコインは歴史的に暗号通貨間の価格変動の主な要因となっていますが、他のコインも市場に影響を与えています。

リターンの予測 金融モデリングの基本的な作業は、近い将来の価格がどのように動くかを予測することである。過去の価格の時系列データを学習データとして、価格が上がるか下がるか、どの程度上がるか、すなわち資産リターンを予測したいのです。 このコンペティションでは、Kagglerは14種類の人気暗号通貨のリターンを予測する機械学習モデルを、数分から数時間の時間スケールで構築することが課題となっています。あなたは、数百万行の分単位の暗号通貨取引データにアクセスすることができ、それを使って14の資産すべてについて同時に予測モデルを設計することになります。あなたの予測は、コンペティション終了後の3ヶ月間の評価期間中に収集された実際の市場データとの相関関係によって評価されます。 暗号通貨のリターン予測は、オープンかつ非常に困難な予測タスクです。資産の極端な変動、データの非定常性、市場やミーム操作、資産間の相関、非常に速く変化する市場状況を考えると、これはMLコミュニティにとって魅力的な問題領域と言えます。私たちと同じように、皆さんもこの問題を魅力的だと感じてくれることを願っています。

  • スケジュール

このコンペティションは、トレーニング期間と実際の市場データに対してモデルを実行する第2期間を設けた予測コンペティションです。 トレーニングのタイムライン 2021年11月2日~開始日

2022年1月25日 - エントリー締め切り。出場するためには、この日までに競技規則に同意する必要があります。

2022年1月25日 - チーム合併の締切。この日が、参加者がチームに参加したり、合併したりできる最終日です。

2022年2月1日 - 最終提出期限。

すべての締め切りは、特に断りのない限り、該当する日の午後11時59分(UTC)です。大会主催者は、必要と判断した場合、コンテストのスケジュールを更新する権利を有します。

予測タイムライン。 最終提出期限の後、選択されたノートブックに対して実行される市場データの更新を反映するために、リーダーボードに定期的な更新が行われます。更新はおよそ2週間おきに行われる予定です。

2022年5月3日 コンペティション終了日 - 入賞者発表

optiver-realized-volatility-prediction 3日目

目標がないと頑張れないので、今日の時点で銅メダルラインの0.1955を一旦に目標してみよう。方法はディスカッションやVote多いNotebookみてひたすらぱくる。

今日のまとめ

  • スコア0.2192 -> 0.2134

今日の目標

  • 前回うまくいったボラのラグを取る特徴量を他の値でも試す

  • クラスタリングしてその平均取るやつをやる。

前回うまくいったボラのラグを取る特徴量を他の値でも試す

realized_volを正しいtime_idに並べ替えて、そのラグをとったらスコア結構あがった。前回はラグ1しかとらなかったので、それを1,2,5...のように複数取るだけ。

クラスタリングしてその平均取るやつをやる。

time_idとstock_idで類似性が高いものをクラスタリングして、その平均取った値を特徴量にする。

サンプルコード

target_feature = 'book.log_return1.realized_volatility'
n_max = 40

# make neighbors
pivot = df.pivot('time_id', 'stock_id', 'price')
pivot = pivot.fillna(pivot.mean())
pivot = pd.DataFrame(minmax_scale(pivot))

nn = NearestNeighbors(n_neighbors=n_max, p=1)
nn.fit(pivot)
neighbors = nn.kneighbors(pivot)

# aggregate

def make_nn_feature(df, neighbors, f_col, n=5, agg=np.mean, postfix=''):
    pivot_aggs = pd.DataFrame(agg(neighbors[1:n,:,:], axis=0), 
                              columns=feature_pivot.columns, 
                              index=feature_pivot.index)
    dst = pivot_aggs.unstack().reset_index()
    dst.columns = ['stock_id', 'time_id', f'{f_col}_cluster{n}{postfix}_{agg.__name__}']
    return dst

feature_pivot = df.pivot('time_id', 'stock_id', target_feature)
feature_pivot = feature_pivot.fillna(feature_pivot.mean())

neighbor_features = np.zeros((n_max, *feature_pivot.shape))

for i in range(n):
    neighbor_features[i, :, :] += feature_pivot.values[neighbors[:, i], :]

for n in [2, 3, 5, 10, 20, 40]:
    dst = make_nn_feature(df, neighbors, feature_pivot, n)
    df = pd.merge(df, dst, on=['stock_id', 'time_id'], how='left')

実装コード

中に入れ子で関数作ったりして、変数の受け渡しがめんどくさかったのでclassつかってみた。いまいちうまく使えているかわからん。。

from sklearn.neighbors import NearestNeighbors

class NearestNeiborFeature:
    
    def __init__(self, f_nn, f_agg, type_):
        self.n_max = 40
        self.feature_nn = f_nn
        self.feature_agg = f_agg
        self.type_ = type_

    def nn_features(self, df):
        '''
        特徴量Xのminmax_scaleで近いもの同士(time_id, stock_id)を集めて、その平均を取る

        - time_idのとき
        time_idが近い=そのときfeature_nnのminmax水準が近い
        各stockに対してtimeが近いn個のfeature_aggを抽出して、その平均取る
        '''
        def make_nn_feature(df, neighbors, n, agg=np.mean):
            pivot_aggs = pd.DataFrame(agg(neighbors[1:n,:,:], axis=0), 
                                      columns=df.columns, 
                                      index=df.index)
            dst = pivot_aggs.stack().reset_index()
            dst.columns = ['time_id', 'stock_id', f'{self.type_}_cluster{n}_{agg.__name__}_{self.feature_nn}']
            return dst
        

        # make neighbors
        pivot = df.pivot('time_id', 'stock_id', self.feature_nn)
        pivot = pivot.fillna(pivot.mean())
        pivot = pd.DataFrame(minmax_scale(pivot))
        if self.type_ == "stock":
            pivot = pivot.T # stockのときのみ転置
        
        nn = NearestNeighbors(n_neighbors=self.n_max, p=1)
        '''
        n_neibors: 近いとして持ってくる個数
        p(int), default=2: 距離の出し方
        Parameter for the Minkowski metric from sklearn.metrics.pairwise.pairwise_distances. When p = 1, this is equivalent to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
        '''
            
        nn.fit(pivot)
        neighbors = nn.kneighbors(pivot) # tuple(0: distance, 1: index)

        # nnで近い順にn個取得、その平均
        feature_pivot = df.pivot('time_id', 'stock_id', self.feature_agg)
        feature_pivot = feature_pivot.fillna(feature_pivot.mean())
        if self.type_ == "stock":
            feature_pivot = feature_pivot.T # stockのときのみ転置
        
        neighbor_features = np.zeros((self.n_max, *feature_pivot.shape))
        for i in range(self.n_max):
            # 1d: nn, 2d: time_id, 3d: stock_id
            neighbor_features[i, :, :] += feature_pivot.values[neighbors[1][:, i], :]

        for n in [2, 3, 5, 10, 20, 40]:
            dst = make_nn_feature(feature_pivot, neighbor_features, n)
            df = pd.merge(df, dst, on=['stock_id', 'time_id'], how='left')
        
        return df

作成した特徴量

  • stock_cluster5_mean_log_return_realized_volatility: クラスタリングしてその平均取ったりしたやつ

importance

地味にorder(時系列の順番)も結構効いている気が。

f:id:iiiiikamirin:20211027231509p:plain

optiver-realized-volatility-prediction 2日目

目標がないと頑張れないので、今日の時点で銅メダルラインの0.1955を一旦に目標してみよう。方法はディスカッションやVote多いNotebookみてひたすらぱくる。

今日のまとめ

  • スコア0.2344 -> 0.2192
  • 時系列+元のpriceに復元したことでラグやtime_id間での変化率が取れるようになった。
  • ラグは1だけにしたけど、もっと他の値でもとるか?
  • あと、クラスタリングしてその平均取るやつもやってみる。結構スコア上がるらしい。

今日の目標

time_idを時系列に復元+priceをもとの値に戻す

time_idを時系列に復元

参考はこのDiscussion.Discussionでやられているのは「time_idの復元」「time_id,stock_idで似ているものクラスタリングしてその平均みたいな特徴量作成」の2つと理解。time_idが復元できたら「ラグ変数の作成」「Cross Variationを時系列で検証」ができるかなと。まずはtime_idの復元を。

Optiver Realized Volatility Prediction | Kaggle

サンプルコード

というかそもそもtime_idってランダムだったの?という。。要点まとめると、 1. 各stock_idのbid/ask priceをtick(最小値幅)で割ると元のpriceに戻る。

  1. priceをminmax scalingして、近い順に並べ替えると時系列順に復元可能。

前者は最初の値における最小単位は1とかですべて共通。しかし、そのsecondsにおける株価の平均などで割られているのでは?割った理由はseriesにおけるボラ(標準偏差)がpriceの水準によって変わることを修正したかったのかなと。x*100の標準偏差はxの標準偏差の100倍なので。

後者はstock_idとあるのでおそらくすべて株=相関高いからこそ可能な気が。それなら時系列順に戻す前でも相関は高そうではある。

import glob

import numpy as np
import pandas as pd
from joblib import Parallel, delayed
from sklearn.manifold import TSNE
from sklearn.preprocessing import minmax_scale


def calc_price_from_tick(df):
    tick = sorted(np.diff(sorted(np.unique(df.values.flatten()))))[0]
    return 0.01 / tick


def calc_prices(r):
    df = pd.read_parquet(r.book_path,
                         columns=[
                             'time_id',
                             'ask_price1',
                             'ask_price2',
                             'bid_price1',
                             'bid_price2'
                         ])
    df = df.groupby('time_id') \
        .apply(calc_price_from_tick).to_frame('price').reset_index()
    df['stock_id'] = r.stock_id
    return df


def reconstruct_time_id_order():
    paths = glob.glob('/kaggle/input/optiver-realized-volatility-prediction/book_train.parquet/**/*.parquet')

    df_files = pd.DataFrame(
        {'book_path': paths}) \
        .eval('stock_id = book_path.str.extract("stock_id=(\d+)").astype("int")',
              engine='python')

    # build price matrix using tick-size
    df_prices = pd.concat(
        Parallel(n_jobs=4)(
            delayed(calc_prices)(r) for _, r in df_files.iterrows()
        )
    )
    df_prices = df_prices.pivot('time_id', 'stock_id', 'price')

    # t-SNE to recovering time-id order
    clf = TSNE(
        n_components=1,
        perplexity=400,
        random_state=0,
        n_iter=2000
    )
    compressed = clf.fit_transform(
        pd.DataFrame(minmax_scale(df_prices.fillna(df_prices.mean())))
    )

    order = np.argsort(compressed[:, 0])
    ordered = df_prices.reindex(order).reset_index(drop=True)

    # correct direction of time-id order using known stock (id61 = AMZN)
    if ordered[61].iloc[0] > ordered[61].iloc[-1]:
        ordered = ordered.reindex(ordered.index[::-1])\
            .reset_index(drop=True)

    return ordered[['time_id']]

作成した特徴量

price_change_timeid: 価格変化率 vol_lag1_timeid: time_id一つ前のrealized volatility vol_20med_timeid: 20日ロール中央値realized volatility

f:id:iiiiikamirin:20211024230817p:plain

importance

20medが効いた。lag1も効いてる。pct_changeが効かなかったか。絶対値が大きければtargetも大きくなると思ったんだが。

f:id:iiiiikamirin:20211024231045p:plain