M5 Forecasting - Accuracy

 序盤のみですが、頑張ったので作った特徴を記念に書きたいと思います。

コンペの概要

主催Walmart

目的:POSデータを用いて各商品ごとの売上個数を予測する (外部データもOK)

特徴:POSデータがマスキングされている。
食品/おもちゃなど、カテゴリーレベルの粗い情報は与えられているが個々の商品情報はマスキング済み。そのため食品に属する商品と言っても加工食品なのか調味料なのかといった細か目な商品情報はわからない。

作った特徴

■価格弾力性

各商品の価格帯ごとに売上個数の標準偏差を価格弾力性の指標として使った。
多分コモディティ商品かどうかの属性情報にもなっていたと思う。

■競合商品との価格差

基本的には競合商品よりも安くなればなるほど、買われやすくなるはずなので、競合商品との価格差は重要な特徴になるはずと考えた。
例)ファブリーズ買おうと思ったけど、半額セールされてたリセッシュ買った 

※煙草のように嗜好性が強いカテゴリーに対しては効かなくて、多分コモディティ化しているカテゴリーでしか有効じゃないと思う。マルボロ吸ってる人は他の煙草ブランドが値下げされようがブランドスイッチしないのがその例。

ただ、各商品はマスキングされていて、競合商品も特定できない。
そのため、”欠品期間”を使って競合商品を特定した。
商品Aが欠品している期間に売上が跳ね上がった商品Bが競合商品であるはず。というロジック。このやり方で各商品ごとに競合商品を特定してdailyで価格差を算出。

■値下げによって心理的節目を割ったか

同じ10円の値下げでも1100→1090円の値下げと108円→98円の値下げでは消費者への心理的な影響が異なるため、値下げによって心理的節目を割った場合は1、そうでない場合0のフラグ付け。

■売り場の面積・質

売り場面積が小さかったり、棚の下段とか、売り場が悪いとそもそも存在にすら気づいてもらえない。反対に売り場面積が大きかったり、いわゆるゴールデンゾーンに置かれてると、それだけで買われやすくなるなど、売り場の良し悪しは各商品の売上に影響を与えるはず。

店側からすると売上に貢献してくれる商品を良い売り場に割り当てるはずで、
単純に売上金額の大きい商品ほど売り場が良くなると仮定。
売上金額は売上個数 × 価格で算出し、各商品ごとの疑似的な売り場の面積・質の良し悪しの特徴とした。
季節性回避のため、使用期間は前年。

 ■その他① train/testで商品の価格帯を揃える

trainでは特売セールされていたが、testではされてなかった。というようにtrain/testで価格帯が異なるのは汎化性能の意味で良くない。
そのため、test期間における各商品ごとのmin-maxの価格帯外のtrainデータを削った。(データを削りすぎたせいか、predict出来なかったので一部のデータは削る条件変えた記憶がある)

■その他② targetを0-1に正規化しxentropy loss最適化

予測値がマイナスになったりしたので、avitoのwinners solutionにもなっていたのを試した。ただ全体のスコアは改善しなかった。
https://www.kaggle.com/c/avito-demand-prediction/discussion/59885

 

【kaggle】IEEE参戦記

ブログを書くに至った経緯/目的

訪問いただきありがとうございます。

私はHomeCreditからkaggleを始めて、kaggle歴は一年程になります。

今回「IEEE-CIS Fraud Detection」に参加し、それなりに頑張ったものの、結果はハタから見るとkernelをForkしただけにしか見えない結果となったのが悔しかったので、試行錯誤した証を残したく、このブログを書き記した次第です。また、kaggleはこの1年ほぼ毎日やっているものの、振り返りは手元のiPadに書いているだけの閉じたものだったのですが、オープンにすることで、間違いを指摘いただける方もいるかもしれないという淡い期待もあります。

 

IEEE-CIS Fraud Detection】概要

概要については、ご存知の方も多いと思うのでざっくりです。

クレジットカードの取引履歴からその取引が不正取引かどうかを予測するコンペ。

特徴①:匿名データ

              432変数ある中、ほとんどは匿名化されていて何のデータかわからない。

特徴②:不均衡データ(画像参照)

f:id:Gig:20191008001943p:plain

 

特徴③:train/testは時系列でsplit(画像参照) 

f:id:Gig:20191008002334p:plain

※横軸は時間に関する変数ですが、加工されていて素の状態だと明確に何日かはわからない。train/test間で一部データが欠損していてギャップがある。

 

特徴④:Adversarial modelのAUCが1に近く、Train/Testでデータ傾向が異なる

※1stのソリューションで分かるが、データ傾向の違いは、単純に時系列性に起因しているのではなく、train/testで重複IDがかなり少ないということが背景にある。

 

コンペの取り組み方

1:他のコンペやる(敢えてね)&情報整理(スクショ&メモ)

コンペの開催期間は約3ヶ月と長く、自分の性格的に集中力が持たなそうだなーと思いました。ダラダラやるよりは締切に追われながら色んなコンペやったほうが成長できそうと思い、終了1ヶ月前まで他のコンペやろうと決めました。とはいえ、コンペ終了1ヶ月前となるとdiscussionには大量に情報が溜まり、一気に情報を吸収するのもキツイものがあります。少しでもデータに触れておいたほうが、後でキャッチアップしやすくなると考え、他のコンペをやる前にとりあえず1~2subだけでもしておこうと思いました。

kernelを見ていると、時間に関する変数であるTransactionDTでGroupKFoldを行っているものが多かったです。train/testが時系列でsplitされているからですね。それを見て、「不均衡データということもあり、Targetがちゃんと入っていないfoldもあるんでは?」と思ったので、PetFinderの時に使われていたStratifiedFoldとGroupKFoldの良いとこ取りをしたValidationを採用しましたが、スコアは上がりませんでした。

Stratified Group k-Fold Cross-Validation | Kaggle

これはPetFinderや他のコンペでも効かなかったので、特に驚きもせず他のコンペに移りました。

 

まずはGANコンペ、その後、土地コンペ(Signate)、APTOSを経てIEEEに戻ってきました。戻って来たときには2ヶ月経過していることも有り、Discussionやkernelが大量にありました。どのコンペでもやっているのですが、まずは大量にある情報の整理/棚卸しから始めました。

情報の整理/棚卸しでやったことについて、下記はその例です。

・そもそも「Fraud」の定義は?:データ理解や解くべき課題・目的の理解を含む。

英語→日本語の翻訳:英語読解能力が低いので、訳した結果をメモリます

・それぞれの変数が意味する所の考察:匿名データなので

起きている問題の把握:CV/LBの乖離など

・起きている問題の中から対処するべき問題の優先順位付け:

・kernelに投稿されてい最高モデルのGainを確認し、効きそうな変数の勘所を探る

 

情報整理の際、いつもiPadを活用しており、画面のスクショ&メモを書いています。

画像はその例です。文字汚くてすいません。

f:id:Gig:20191008024203j:plain

 

f:id:Gig:20191009201640j:plain 

f:id:Gig:20191009201504j:plain


あとは、まだディスカッションに出ていない特徴をつかむためにも、ゼロベースでやんわり構造理解とファネルごとの仮説・検証ポイントを考えてメモしていました。

下記はその例。

「縦軸が意味不明」、「MECEになってねぇだろ」とか思われるともいますがラクガキなので許してください笑

f:id:Gig:20191008024147j:plain

文字が汚くて読めないと思いますが、カードが詐欺師に渡るまでの段階ごとに下記のような仮説と作る特徴量の考察をしています。

第一段階:カードを紛失しやすい人/そうでない人で属性に違いがある?

第二段階:カード紛失時の戻ってきやすさはエリアごとに違いがある?(日本だと戻ってきやすいためFraud率も低い/そもそも国ごとに犯罪率は大きく異なる)=エリア別(addr1-2)にTarget Encodingが有効?

第三段階:カード紛失前後でユーザー(買い手)が変わる

=取引の前後で購入する物≒購入金額が変わる or IPアドレスが変わるはず

IDごとにdiff使った変数が有効?

 

こんな感じで情報収集と仮説構築を行い「validationどうする?時系列だし・・・」とか考えて、色々試行錯誤しましたが、文字数の関係で枝葉の部分はこのブログでは書きません。このコンペは下記2つが大きなテーマだと考えたので、2つのみ説明していきます。

・UniqueIDの特定

・データの時系列性を弱める特徴量エンジニアリング

UniqueIDごとの攻めの特徴でpublic上位に行き、時系列性を消す守りの特徴でShake Downを防ぎ、privateでも上位を守りたいなと思ってました。

最初はさっさとUniqueIDを特定して、データの時系列性を弱める特徴量エンジニアリングに時間をかけるつもりでした。まるで叶わなかったですが・・・

コンペの取り組み方

 2:UniqueIDの特定

UniqueIDの特定が重要だと思いました。理由は下記3点。

・直感 (人に関するデータだし、ID使えば効くでしょって思ってた)

・当時のKernelで最高スコアを出していたモデルのGain上位はUniqueIDに関する変数であり「UniqueIDを使った変数は効く」という事実があったのですが、その「UniqueID」はカード会社の属性を表す変数や請求先住所(card1-6やaddr1-2とか)を結合していただけのものであり、まだまだUniqueIDの特定には改善余地があると思った。

IEEE - FE with some EDA | Kaggle

・サブモデルが有効なソリューションになるコンペは多く、サブモデル使えば勝てるんでは?と安直に思っていて、そのためにUniqueID特定する必要がありました。過去のコンペでは、IDごとに予測値のmin/mean/maxを取る手法が効いていたのでこれを試そうと思った。(HomeCredit 17th /ELO 1stの手法)

17th place mini writeup | Kaggle

 

Discussionnには匿名データの意味する所として下記の可能性がある記載がありました。

D1 : クレジットカード登録日からの経過日数

D3 : 前回クレジットカード利用日からの経過日数

これを見た時、経過日数を四則演算で解いていって、マッチングする人を見つけていけばUniqueIDを特定できる事に気づいたので、card1-6に加えて、D1とD3を活用してUniqueIDを特定していこうと思いました。

※MailAdressなどその他にも個人が特定できそうな変数はあったが使っていない。

カード紛失→詐欺師が拾った場合だとtransactionの前後で別人のIDが割り振られてしまうことがその理由で、IDごとにDiff使った特徴作った際に悪影響になると考えたから。

ただ、これだけでは不完全だと思いました。

なぜなら、card1-6まで結合させたグループ間であっても、D1=0かつD3=NAN、つまりクレジットカード登録日に即利用した人を始め、別人を同一人物としてUniqueIDを割り振ってしまう恐れがあったためです。上記を回避するため、他にユーザーを絞っていけそうな変数を足すことにしました。

データを眺めているとV126列は購入金額の値と一致しているものが多い事に気づきました。KernelやDiscussionで「V列は何かしらの数字の累積なんじゃないか?」ということがよく言われていたこともあり、V126を「UniqueIDごとの購入金額の累積値」と仮定しました。上記4つの変数の四則演算で解き、ユーザーを特定し、

作成したUniqueIDを用いてmin/mean/std/maxなどのaggregationを行いましたが

まるでスコアは上がりませんでした。

色々試行錯誤したので、ここで大量に時間を溶かします。

 ※コンペ終わってからV126の意味は9位が説明してくれてます。まるで違いました笑

まぁ「V列に累計値が多い」という話があっただけで「V126が」とは言ってないから当たり前ですね・・・

9th place solution notes | Kaggle

 

V126がダメだったので、他にそれっぽい変数を探し、V307が累積の購入金額に見えたので今度はそちらを使いました。紐付けられるID数も格段にあがり、目視でもそれっぽい人が大量に見つかりました。

下記は97ドルでしか購入していない人のシンプルな1例です。

Day(経過日数)D1(登録日からの経過日数)、D3(前回利用日からの経過日数)、TransactionAmt(購入金額)、V307(累積購入金額)が一致しています。

f:id:Gig:20191009162852p:plain


が・・・

この方法も間違いです。なぜならV307は”各月の”累積購入金額だったからです。翌月になるとV307は0にリセットされ、前月-当月間でV307の連続性が消滅し、同一人物であっても別人のUniqueIDが割り当てられれます。この辺の解説は下記29thの解説が詳しいです。

29th place - Mini write up about automation and groupings | Kaggle

データチェックはしたのですが、上の行の12月や1月のデータ、つまりV307がリセットされる前の間違いがあまり無いデータのみしか見ていなかったので、きちんとUniqueIDが紐付けられていないことに気づいていませんでした。

 このUniqueIDごとにmin/mean/maxなどの特徴を作ると、V126の時よりは精度が向上しましたが、間違った方法だったので、0.946程度しか精度が出なかったです。

uniqueIDもどきが手に入ったこともあり、念願のサブモデルを試みましたが、やたら早くEarlyStoppingがかかりました(確か27くらい)。ビビってそのままサブミットはせずに、meanやmaxは削って、minだけにして学習させてサブミットしましたがスコアは向上しませんでした。

 

他にもこのUniqueIDを使い、色々試行錯誤しましたが、うまくいきませんでした。

UniqueIDよりも次の時系列性を弱めることの方が遥かに重要だと思ったので

時系列性を弱める特徴量エンジニアニングに移りました。

コンペの取り組み方

 3:データの時系列性を弱める特徴量エンジニアリング

概要で記載したとおり、このコンペは時系列データであり、Adverarial modelも

train/testでデータ傾向が大きく異なることを示していました。

コンペ中はtrain/testの傾向の違いは時系列性から来ていると認識しており

「時系列性」は対処すべき問題として捉えていました。

下記に対処した一例を挙げます。

adversarial modelのgainを見ると、topはid_31(ブラウザのversion)でした。

f:id:Gig:20191008234712p:plain

id_31は下記の画像のように一定期間ごとに新しいversionが出ていて、train/testで重複がない場合もありました。かつ、最新のversion使用者の方がfraudしやすいという特徴もありました。これについては「詐欺師は技術に明るくversion更新にも余念がないんでは」と言われていた。確かに最新のchrome使ってる方がうまく詐欺が行えそうなので納得してました。

(いやFraud=検知されてるやんけ!というツッコミは無しで・・・)

f:id:Gig:20191008233031j:plain

train/testで分布が違って有害な変数という事で、ターゲットに効かないのであれば消せばいいですが、効く以上うまく加工して残そうと思いました。そこで「”月ごとに”何番目に新しいversionか」という特徴に加工しました。これでターゲットへの効き方を残しつつ、時系列性を薄めた。ちなみに時間が無かったので外部データを使ってversionのリリース日は参照していなくて、最初にデータ発生した日を擬似的なリリース日とした。重複した場合は記載されているversionの番号を参考にした。

Ex)Chrome 40とchrome41のデータ発生日が同じであれば、chrome41を新しい方とする

 

あとはDiscussionでChrisが書いている週ごとの平均で各値を引いたり・・・

df['D3_remove_time'] = df['D3'] - df['D3_week_mean']

Feature Engineering Techniques | Kaggle

 

前月との増減値を全体とUniqueIDの比率を取って、全体の傾向を消しつつ、

UnqueIDごとの個性を出せるようにしました。

意味不明だと思うので、簡単な例を出します。

通常であれば、前月比で30%も購入金額が上昇しているIDがあれば異常ですよね?

でも12月のクリスマスシーズンであれば自然です。平均の人が40%プラスであれば

むしろ、購入金額が上がらなかった人になります。

この例で言うと、30%÷40%=0.75をそのIDのスコアとする。

というような特徴量エンジニアリングです。

 

アソシエーション分析のイメージに近いでしょうか。

全体の傾向(時系列性)を消しつつ個性を際立たせる感じの特徴作りを意識してました。

まぁ時間なかったんであんま作り込めなかった&これらはまるで効いていなかったです。

 

最終サブミットはShakeに備えてValidationの切り方が異なる2つを選びました

①Hold Out 

train/Validation set間で1ヶ月空けたもの(train/test間でも間が空いていたので下記akasyanamaのDiscussionを参考にした)

Train/Validation split | Kaggle

②monthごとのGroupKFold

※TimeSeriesSplitは精度でなかったので不採用。 

それぞれ、LGBM,CatBoostのSeed違い、合計4モデルのblendingでした。

評価指標的にrank averageを選択。

 Stackingはvalidationが信じられなかったのでやってない。

EnsembleにLGBMとCatboostを選んだ理由は下記の消去法から。

XGboost

・LGBMとEnsembleの相性悪い説があるので

(試してすらいないが、上位陣は使っていた。Do Everhthingの言葉が染みる)

NN

・時間無さすぎて木系と異なる処理に時間掛けたくなかった

 

 

10月4日9:00、コンペ終了を迎えました。

結果は・・・

 

1370位(6381中)でした。

shakeが予想されるコンペではありましたが、結果が出る前に負けは確信していました。下記はコンペ終了1分前のツイートです。

APTOS、IEEE共にDeadLine延長の洗礼を受けて、睡眠時間削りまくった割に結果が出なかったので、この時は魂抜けてました。

f:id:Gig:20191008222013p:plain

 ちなみにですが、この時のV列が指しているのはV126で、まだV307は正しかったと勘違いしてます。どっちも間違ってるよ笑

 

その他

効いた事・特徴

次元削減:LOFOやPermutation Importanceの結果を参考に一部V列とid列を削除

因みにですが、出てくる結果が変わらない割にLOFOはめっちゃ遅いので

permutation Importanceの方がお手軽でいいかなと思いました。

Centに循環性を持たせた特徴量

下記の画像のように、一部のプロダクトでは、特定のcentでfraudしやすい傾向があり、何か特徴作れんかなと思って、sin/cosinで循環性持たせるように特徴作ったらちょっと効きました。 centだと0-99で分断されてしまっているので。f:id:Gig:20191009005205p:plain

 

この手法は通常時系列データで使われるようです。ちなみに

曜日、時間帯でも試しましたが効きませんでした。

SIGNATE 国立公園の観光宿泊者数予測に参加しました | リクルートテクノロジーズ メンバーズブログ

 

効かなかった事・特徴(まだあんまりkaggleに挙がってないやつだけ記載)

次元削減:Adversarial Modelを参考にid_31やid_13などのgain上位変数を削除したが、CV/LB共に減少。

 ・Pseudo labeling

学習データをtestに寄せる

Adversarial modelのスコアを参考に、よりTrain-likeな学習データを削除し、Test-likeなデータで学習させたが、CV/LB共に大きく減少。

不均衡データということもあったので、Train-like、かつ負例のみを削除すれば

結果は違う可能性は頭をよぎっていたが優先順位が高くなかったので試していません。(効くのであればNegative Down Samplingも兼ねれるので二度美味しいですけどね)

 

・「UniqueIDごとに次の取引データまでの分数」特徴量

作った理由:次の取引までの分数が短いほどFraudしやすいように見えたので。あと「TalkingData AdTracking Fraud Detection Challenge」では「Next Click」が重要だったから

効かなかった理由の考察:既に似た特徴が入っている(ex:D3=0は同日複数の購入があったことを意味している)

 

・「UniqueIDごとに一日ごとの取引回数」特徴量

作った理由:上の理由と近い。詐欺師は短期間に高頻度で発注しているように見えたから。一度に大きな金額を発注すると、一般的な購買行動と異なり不正検知されやすくなると詐欺師が知っていて、不正検知をかいくぐるために、分割発注してるんでは?と思ってた。

 

自省・今後改善するポイント

1 :対処すべき問題の特定(経験から来るセンス?)

初の時系列コンペということで「対処すべきは時系列性とバリデーション。ここに時間を割くべき」と素人ながら「分析のキホン」に忠実になりすぎた所が敗因として大きいです。1stのchrisも述べているように、このコンペで時系列性への対処は重要ではありませんでした。分析に入る前にイシューの見極めの時点で私の負けは確定していました。

時系列系を抑える特徴よりも、UniqueIDの特徴の方がスコアの変動が大きい所からどちらが重要か気づけたはずでした。セオリーを疑い、問題に対処するというのも必要でした

 

2:コーディングスキル

コンペで重要な役割を果たしていたuniquIDもデータチェックを適切なやり方でやっていれば間違いには気づけたはずです。データチェックのコードも学んでいきたいです。

UniqueID特定方法が記されてたKernelのコードが大量に公開されているので、kernelのコードは読み込んで次に生かしていきます。

 

 3:自制

枝葉の問題にかける無駄な時間を使うことに対して、自制が利かなかった事も問題でした。今回、時系列への対処が肝であると認識していた割には、UniqueIDの特定に時間をかけてしまいました。このコンペではUniqueIDの特定が肝だったので、結果だけ見ればその選択は正しいのですが、 自ら決めた時間配分を守れず、(その時は)優先順位が低いものに時間を費やしたのは事実です。思ったよりもUniqueIDの特定がうまくいかず、投入時間が増えてしまったことでコンコルド効果が働いたのだと思います。

 

上記は改善ポイントの一例ですが、上記マストで改善しつつ次回はメダルを取りに行きたいです。