Time-series code competitionで
生き残るには
2021/08/17 第3回分析コンペLT会
Nomi (能見)
Kaggle master / @nyanp / @nyanpn
Software engineer
Recent 5 competitions:
暫定7位
★: Time-series code competition
★
★
★
Time-series code competition?
▲Riiidでの表記 (細かい表現はコンペによって異なるが、Dataタブで確認できる)
どんな仕組み?
import riiideducation
env = riiideducation.make_env()
iter_test = env.iter_test()
for (test_df, sample_pred_df) in iter_test:
# 前処理&予測
sample_pred_df['target'] = 0.5
# ここで予測値を提出
# predictを呼ばずに次データを取ろうとすると例外
env.predict(sample_pred_df)
どんな仕組み?
import riiideducation
env = riiideducation.make_env()
iter_test = env.iter_test()
for (test_df, sample_pred_df) in iter_test:
# 前処理&予測
sample_pred_df['target'] = 0.5
# ここで予測値を提出
# predictを呼ばずに次データを取ろうとすると例外
env.predict(sample_pred_df)
実際大変
A. ストリーム処理に対応しつつ、
高速でロバストなコードを書くのが大変
Q. Time-seriesコンペ、何が大変?
Q. Time-seriesコンペ、何が大変?
A. ストリーム処理に対応しつつ、
高速でロバストなコードを書くのが大変
とはいえ、ポイントを押さえれば全然怖くない!
本日のお品書き
Time-seriesコンペで生き残るために重要な4つのポイントについて、個人的に気を付けている点を共有します
1. Train/Testの両対応
2. 状態管理の設計
3. コード構成とデバッグ
4. エラーハンドリング
※スタイルの好みやコンペ毎の差もあるので、一意見として見て下さい
本日のお品書き
Time-seriesコンペで生き残るために重要な4つのポイントについて、個人的に気を付けている点を共有します
1. Train/Testの両対応
2. 状態管理の設計
ストリーム処理に
対応しつつ
高速でロバストな
コードを書く
3. コード構成とデバッグ
4. エラーハンドリング
※スタイルの好みやコンペ毎の差もあるので、一意見として見て下さい
Train/Testの両対応
1
Train/Testの両対応
submission.csv
※説明の簡略化のために1行としていますが、1ループで同時刻の複数データが来ることも(コンペによる)
よくある実装方針
①Train向けのバッチ処理関数を、Testにも使う?
②Train向けとTest向けで別々のコードを書く?
よくある実装方針
①Train向けのバッチ処理関数を、Testにも使う?
②Train向けとTest向けで別々のコードを書く?
× バッチ用処理を無理やりTestに使うと遅くなりがち
× Testの予期しない入力で死にがち
× 実装の違いでバグを生みがち
× アイデアの実装が億劫になりがち
よくある実装方針
①Train向けのバッチ処理関数を、Testにも使う?
②Train向けとTest向けで別々のコードを書く?
× バッチ用処理を無理やりTestに使うと遅くなりがち
× Testの予期しない入力で死にがち
× 実装の違いでバグを生みがち
× アイデアの実装が億劫になりがち
(最終形としては)両方おすすめできない
おすすめ:全部ストリーム処理
実例: MLBコンペ
@feature(['divisionId', 'divisionRank', 'leagueRank', 'wildCardRank'])
def f015_latest_standings(ctx: Context) -> Dict:
if ctx.team is None or len(tx.team.standings) == 0:
return empty_feature(ctx.current_feature_name)
standings = ctx.team.standings
return {
'divisionId': standings['divisionId'][-1],
'divisionRank': standings['divisionRank'][-1],
'leagueRank': standings['leagueRank'][-1],
'wildCardRank': standings['wildCardRank'][-1]
}
デコレータで作る特徴量を宣言
Contextには状態管理クラス(後述)のインスタンスと、行を特定する情報(MLBでは日付✖️playerId)が入っている
デコレータの中身
def feature(columns: List[str]):
def _feature(func):
@functools.wraps(func)
def wrapper(*args):
ctx = args[0]
assert isinstance(ctx, Context)
assert len(args) == 1
ctx.current_feature_name = func.__name__
try:
return func(ctx)
except Exception:
msg = f"WARNING: exception in {func.__name__}: {traceback.format_exc()}"
warnings.warn(msg)
# guard
if ctx.fallback_to_none:
return {c: None for c in columns}
else:
raise
_FEATURE_COLUMNS[func.__name__] = columns
_FEATURES[func.__name__] = wrapper
return wrapper
return _feature
特徴量関数の中で例外が出たら、
欠損値にフォールバック
(本番の推論時のみ)
デコレータ側でグローバル変数に特徴量の名前やスキーマ情報を自動登録
特徴量生成関数
def make_feature(base_df: pd.DataFrame, store: Store, feature_list: List[str] = None):
feature_functions = {normalize_feature_name(k): get_feature(k) for k in feature_list}
features = []
for i, row in tqdm(base_df.iterrows()):
date = row["dailyDataDate"]
player = store.players[row["playerId"]].slice_until(date)
ctx = Context(store, player, date)
feature = {}
for fname, func in feature_functions.items():
feature.update(func(ctx))
features.append(feature)
return pd.DataFrame(features)
デコレータで登録しておいた特徴量生成関数の実体を引いてくる
1行毎に特徴量生成関数を順番に呼ぶ
何が嬉しいのか
状態管理の設計
2
状態管理って?
銀の弾丸は無いが…
どんなデータ構造で状態を持つ?
※生Listと同じ、ならし計算量O(1)。データが溜まるほど重くなる処理は推論時間の見積もりが難しい為、定数時間でappendできると嬉しい
どんなデータ構造で状態を持つ?
※生Listと同じ、ならし計算量O(1)。データが溜まるほど重くなる処理は推論時間の見積もりが難しい為、定数時間でappendできると嬉しい
いや…ここまでの話…
めちゃくちゃ面倒くさそうでは?
ここまで頑張った実装必要?
コード構成とデバッグ
26
3
コードをどこで書くか
参考:
手元でのデバッグ
ベンチマーク
エラーハンドリング
4
コードコンペとエラー
Time-seriesコンペで
最低限やること
for test_df, sample_df in iter_test:
try:
# 処理
...
except Exception:
pass
# 何があっても推論は死守
env.predict(sample_df)
やっておいた方が良いこと
心構え: 暗黙の仮定を意識する
2nd stageのデータ仕様について、Kaggleでは十分な情報が与えられないことが多い。自分のコードに暗黙の仮定が入っていないか注意
NFLで実際に発生してDiscussionが荒れた
MLBでは1日に複数試合する選手が稀にいた
MLB→APIの説明を見ると、1行ずつとは書いていない
ここまでをまとめたコード例
実例: 推論Notebookの個人的な基本形
def in_kaggle():
return 'kaggle_web_client' in sys.modules
if in_kaggle():
import mlb
else:
mlb = Emulator(...)
env = mlb.make_env()
iter_test = env.iter_test()
store = Store(...)
for test_df, sample_prediction_df in iter_test:
try:
# 状態の更新
store.update(test_df)
# 特徴量の作成
feature_df = make_feature(sample_prediction_df, store)
# 推論
sample_prediction_df['target'] = predict(feature_df, model)
except Exception:
warnings.warn(traceback.format_exc())
env.predict(sample_prediction_df)
Kaggle環境なら実物、ローカルならEmulatorを使う
中で例外が起きても、predictは確実に行う
推論用dfへの代入は1回にまとめる(例外安全性の担保)
具体的な処理は全て.pyに追い出しておく
状態はこのStoreで一元管理
まとめ
Message
38
関連リンク
39
Kaggle Days Tokyo, “How to succeed in code (kernel) competitions”
https://www.youtube.com/watch?v=HCg_ewbNEss
My solution of MLB player digital engagement prediction
Discussion from NFL Big Data Bowl, “What kind of effort did you make to make your script robust?”
https://www.kaggle.com/c/nfl-big-data-bowl-2020/discussion/120375
Discussion from Riiid Answer Correctness Prediction, “A few notes...”
https://www.kaggle.com/c/riiid-test-answer-prediction/discussion/196942