signate 公園コンペ で8位でした。

自己紹介

はじめまして。最近、copypasteとしてtwitter, signate, kaggle を始めたものです。
ブログ執筆にも前々から興味はあったのですが、書くネタが思いつかない&書くのが面倒 という理由で一歩踏み出せずにいました。(「アウトプットは大事!」という話は何度か耳にしましたがどうしても半信半疑..)
ただ、コンペに参加する中で様々なcompetitorさんやengineerさんのブログやtweet(その他にもyoutube, kaggle kernel, 書籍などなど)に助けられたので、少しでも還元できればという気持ちと、頭の整理、備忘録も兼ねてブログデビューを決めました。
今後もデータ分析周りのトピックで何かしら書いていけたらな〜と思っております。よろしくおねがいします。

コンペ概要

コンペのテーマは「国立公園の観光宿泊者数予測」でした。
予測対象は下記8つの国立公園で、公園ごとに1年間(2017-01-01 ~ 2017-12-31)の観光者数を日毎に予測します。
学習データの期間は 2015-01-01 ~ 2016-12-31 なので、2年間のログデータを使用して直近1年間を予測することになります。

実用性を重要視した問題設定で、前日以前のデータを元に翌日以降の観光者数を予測するモデルを作る必要がありました。(当たり前といえば当たり前ですが)
要は、kaggle で時折見られる評価データを用いた特徴抽出(train, testをconcatした上で統計量算出など)は禁止されていました。
私としては実問題に近いタスクのほうが取り組む意欲が湧くので、このような問題設定はありがたかったです。

目的変数は公園毎・日毎の観光者数で、評価指標はMAEでした。 公園毎に目的変数の推移や値域が異なっていたので、"公園毎の性質の違いをどうモデルに組み込むか"も一つ重要な点だったのかもしれません。
目的が明確、データの欠損もない、公園毎のサンプル数も均一、データサイズも小さい、ので初学者には取り組みやすいテーマ&データだった印象です。唯一やりにくいと感じたのはサンプル数が若干少ないことくらいでしょうか。(学習セットは公園毎に2年×365日=730サンプル程度)

ちなみに賞金は無く、上位三名には懸賞として国立公園招待券がプレゼントされます。
私は沖縄の公園を満喫するべく、1位目指して毎日公園のことを考え続けました。(初参加&初優勝って誰もが憧れる展開じゃないですか??)
まぁ結果的に大差で負けてしまったわけですが。。

データ概要

データは観光者数のログデータに加えて、以下の外部データが与えられました。

  • SNSデータ(株式会社ホットリンク)
  • ロケーション付SNSデータ(株式会社ナイトレイ)
  • メッシュ型流動人口データ(株式会社Agoop)
  • 公共交通検索ログデータ(ジョルダン株式会社)
  • 国別月別来訪者数集計データ(株式会社コロプラ
  • 気象データ(気象庁
  • 積雪気象観測データ(防災科学技術研究所(NIED))
  • 公共交通検索ログデータ(ジョルダン株式会社)

気象データ(温度、湿度、天気など)や SNSデータもあり、データのリストを見たとき「面白そうだな〜、いろいろ活用方法が考えられそうだな〜」なんて思った記憶があります。(結局一つも使いこなせず、使用しませんでしたが...)
公共交通検索ログデータに至ってはダウンロードすらしておらず、中身すら確認していなかったことにコンペ終了後に気が付きました。
twitter情報によるといくつかの公園では特徴量として有効だったそうで間抜けな自分を恨みます。分析を始める前にしっかりとデータを確認することが大事ですね。。

解法

私の解法を端的に述べると「前年の観光者数を特徴量にしたlightgbm」です。
唯一工夫した点は、とても強引なOver Samplingくらいでしょうか。
取り組んだ順に解法をまとめたいと思います。

public LB: 6000台 [1サブで優勝を狙うも撃沈]

記念すべき最初のサブミットで上位を狙おうとしていました。(1サブで優勝って誰もが憧れる展開じゃないですか??)
結果、しょーもないバグで失敗に終わりました。バグの内容も覚えていません。
そもそも1サブで優勝なんて私にはできるわけないのです。

public LB: 2100台 [ベースラインモデル完成]

バグを修正したところ2100台でした。
モデル構築の方針ですが、私は「前年のログデータから予測日と類似した日付を参照し、類似日の情報を使って予測するモデル」を構築しようと考えました。
具体的には、2017-01-22の予測時には2016-01-22の観光者数を参考にすると良いだろうと考えたわけです。(2016-01-22と2015-01-22は類似していると考える)

この方針でモデル構築するために、サンプルを以下のように準備しました。

  • 学習データ
    • 2015 -> 2016 (2015年の履歴を参考に、2016年の観光者数を予測)
    • サンプル数は365(日)×8(公園数)= 2920になる。
  • 評価データ
    • 2016 -> 2017 (2016年の履歴を参考に、2017年の観光者数を予測)

このサンプルの作り方だと2015年のデータが準備できず(2014年のデータがないため)、サンプル数が半減する不利益がありますが、今回は類似日の目的変数が特徴量として使用できる利益を重視しました。
次に類似日の定義ですが、今回は以下の項目が一致する日付を類似日とし、項目ごとにgroupbyした基本統計量(頻度、最小、最大、平均、パーセンタイル)由来の特徴量を作成しました。

  • 曜日
  • 週数(weekofyear)
  • 週末フラグ
  • 祝日フラグ
  • 休日フラグ
  • 最近傍の休日までの日数
  • 連休フラグ
  • 連休何日目か
  • 月×曜日
  • 月×週数
  • 月×週数
  • ....などなど

カレンダー情報の取得にはjpholidayを使いました。
予測時は目的変数に対数スケール変換(np.log1p)をしたほうが良かったです。
あとはLightGBMに突っ込んでおしまい。
LightGBM は MAE を直接最適化できるので便利ですね。

public LB: 1900台 [CV探索]

次に有効なCV探しをしました。
ここまでは特に何も考えずランダムに5-Foldしていたのですが、コンペではCVの切り方が大事と聞いていたのでいくつか試しました。(実務でも勿論大事なわけですが)
以下が試したものです。

  • Random Kfold
  • Stratified Kfold
    • groupに公園を指定
    • groupに月を指定
    • groupにweekofyearを指定
    • groupに公園×weekofyearを指定

結局、weekofyearをgroupにした Repeated Stratified Kfold にしました。5Fold×5ですね。
TimeSeriesSplitについては、1年通してまんべんなく学習させたい気持ちが強かったので試しませんでした。(groupに月を指定したStratified Kfoldがうまく行かなかったことも理由の一つ)
CVを変えただけでもスコアが伸びたので、データの渡し方の重要さを学びました。 (納得いくCVは結局作れなかったのですが...)

public LB: 1700~1800台 [特徴量探索]

過去の目的変数の集計値が有効なら、それを加工したデータの集計値も有効だろうと考えました。
具体的には移動平均や季節性の排除などをして、時系列データを加工しました。
加工した後は、これまでと同様に基本統計量由来の特徴量を作成します。

さらに、364,365,366日のラグ特徴量も追加しました。

public LB: 1600 ~ 1700台 [Over samplingに挑戦]

サンプル数が少ないことが気になったので、Over Samplingしました。
かなり強引ですが時系列を無視して、サンプルを水増ししました。
試した組み合わせは色々です。
2015.5年は2015年と2016年の中点をとって作成しました。

  • 2016 -> 2015
  • 2015 -> 2015.5
  • 2015.5 -> 2016
  • 2015.5 -> 2015
  • 2016 -> 2015.5
  • ....(2015.2や2015.7など作って試行錯誤)

結局、日光以外の7公園は 2015->2016, 2016->2015, 2015.5->2016で学習し、日光は2015->2016, 2015.5->2016で学習しました。
日光を別にモデリングした理由は、日光だけ2015年と2016年で目的変数の分布が異なっていたためです。
2015年から2016年にかけて増加傾向が見られたためか、逆向きのOver Samplingは逆効果でした。

public LB: 1500台 [各種パラメータ調整]

特徴量選択やLightGBMのハイパラチューニングなどしました。
ちょうどoptunaが公開された時期だったので少し試してみましたが、結局手動チューニングが一番よかったです。
kaggle kernelからLGBMのパラメータを拝借して微調整するだけで十分でした。
optunaの使い方が悪かっただけかも(ほとんど枝刈りしていて全然探索してもらえませんでした。)

終結

最終スコアは public LB:1583.5, pribete: 1827.9 でした。
1位の方は puplic: 1371.6 pribate:1585.2 なので public, pribate共に大敗です。

f:id:copypaste_ds:20181221234640p:plain

その他に試したこと

  • 公園毎にモデル構築
  • 天気を考慮した時系列加工(台風などを考慮したかった)
  • ノイズを加えたOver Sampling
  • stacking
  • 公園ごとに学習サンプルに重み付け

試してみたかったこと

  • 時系列データ用の特徴抽出ライブラリ
  • 外部データの使用
    • 3カラムのデータだけでも試したいことが沢山あったので外部データにあまり目が行きませんでした。

他の方の解法

実際に取り組んだコンペの別解法はとても勉強になります。
2つともサンプル作成方法から私と異なっていて驚きました。
kaggleのようにチームが組めたなら、このようなモデルとアンサンブルすると良いのかもしれませんね。
自分にはなかったアイディアが多くあるので次に活かそうと思います。

おわりに

はじめてコンペを完走できて嬉しい反面、悔しい気持ちも強いです。
自分の立ち位置が少しわかった気がするので、次はさらに上位を目指して頑張ろうと思います。
最後にブログデビューの後押しをしてくださったupuraさんに感謝申し上げます。
次回はkaggleについて何か書けたらな〜と思っております。
アウトプットの大事さを肌で感じられると良いな〜