SIGNATEコンペの振り返り


## はじめに

SOTA Challenge[SIGNATE Cup　2024]に参加して銅メダルを「獲得していなかった為」、解法および自身の振り返りのために書きました。獲得した為と書き始めたかったのですが......。

獲得してませんでした...というのも、チャレンジ期間終了後のLBを見てギリギリだけど何とかメダルに手が届いたと胸を撫で下ろしていたのですが、１週間経ってもメダル付与されないんですよね。で問い合わせてみたら、「チャレンジ期間終了時点で最終評価が確定している投稿者数に対しての順位が上位10％内」なら（1000人なら100位内）メダル獲得ということだった、と。つまり「全投稿者数に対してなら銅メダル水準内」だけど、「最終評価が確定した投稿者数に対しては銅メダル水準に届かない」という事が判明しまして、意気揚々と書いていたのですが、途中で悲しい結果である事がわかった次第です。

すべり込みセーフだったけど運も実力！と自分を鼓舞してましたが、ギリギリ上位10%には入れないことも実力ということで、振り返りをしながら次に進もうという記事になります。

progateから始めて一年、コンペ参加歴は半年程ですが、独学(本や動画等）でChatGPTやGeminiに助けられたりハルシネーションに惑わされたりを繰り返しながらも構築していきましたので、稚拙な箇所が多々あるとは思いますがご容赦くださいませ。


https://signate.jp/competitions/1523

## 経緯
実は、元コンペにもリアルタイムで参加しておりこの時のLBの結果は上位60%(上位をつけるだけでなんか良さげに見える）でした。参加自体が締め切り2週間位前だったこともあり、あまり試せずに終わってしまい消化不良だったため、改めてSOTA Challengeに参加して、色々試したいと取り組んでまいりました。元コンペ742位→122位(上位10%には入れませんでしたが）と躍進できたことは良かったです。


## コンペの概要
（SIGNATE Cup 2024　タスク説明より）「旅行会社の保有する顧客データ（属性や志向、営業担当との接触履歴等）を元に、旅行パッケージの成約率を予測するモデルを構築していただきます」
ということで成約率の予測、評価方法はAUCでした。

## 前提
・実行環境はGoogle Colaboratory上で。
・とにかく色々試そうと思いEDAやモデルごとにファイルを分けて実施しました。

:::note warn
submitファイルの手順を保存する前にすぐに変更を加えており、環境管理が不十分だったこともあって完全に同じ結果が再現できませんでした。その為ソースコードの方はここで紹介しようとしたSubmitしたコードとは異なります(後半でその辺りのことも記載致しました）
:::


## 1回目の参加時と2回目の参加時の違い
元コンペの上位入賞者の解法を参考にできたのは大きかったかな、と思います。不均衡データに対する各モデルのパラメータ設定は参考にさせて頂きました。また、1回目参加時にある程度の前処理は済んでいたのでそのまま運用できるところも多く、かなりの時短に繋げられました。これによりあれもこれも試せる！となるはずでしたが、終わった時の感想は時間が足りなかったであり、結局そういうのはキリがないなと感じた次第です。



## データ概要

### 前処理

<table>
  <caption>元配布データと基本的な前処理一覧</caption>
  <thead>
    <tr>
      <th>特徴量ヘッダ名称</th> <th>説明</th> <th>前処理の実施</th>
    </tr>
  </thead>
  <tr>
    <td> id </td> <td>営業リストの顧客ID</td> <td>なし</td>
  </tr>
  <tr>
    <td> Age </td> <td>顧客の年齢</td> <td>漢数字等削除・変換、数値に統一</td>
  </tr>
  <tr>
    <td> TypeofContact </td> <td>顧客への連絡方法</td> <td>OrdinalEncoder、Nanに-1を割当て</td>
  </tr>
  <tr>
    <td> CityTier </td> <td>都市層(1>2>3)</td> <td>一先ずそのまま</td>
  </tr>
  <tr>
    <td> DurationOfPitch </td> <td>営業担当者による顧客への売り込み時間</td> <td>分単位に変換し数値で統一</td>
  </tr>
  <tr>
    <td> Occupation </td> <td>顧客のご職業</td> <td>OrdinalEncoder</td>
  </tr>
  <tr>
    <td> Gender </td> <td>顧客の性別</td> <td>文字表記を揃えてonehot encoding</td>
  </tr>
  <tr>
    <td> NumberOfPersonVisiting </td> <td>一緒に旅行を予定している人数の合計</td> <td>そのまま</td>
  </tr>
  <tr>
    <td> NumberOfFollowups </td> <td> セールストーク後に営業担当者が行ったフォローアップの合計数</td> <td>100以上は一先ずそのまま</td>
  </tr>
  <tr>
    <td> ProductPitched </td> <td>営業担当者による商品の売り込み</td> <td>文字表記の統一後、各意味合いに沿ってrank化した数値へ</td>
  </tr>
  <tr>
    <td> PreferredPropertyStar </td> <td>顧客によるホテル施設の優先評価</td> <td>そのまま</td>
  </tr>
  <tr>
    <td> NumberOfTrips </td> <td>顧客の年間旅行数</td> <td>文字表記は削除変換等して年間の旅行回数に統一</td>
  </tr>
  <tr>
    <td> Passport: </td> <td> パスポートの所持</td> <td>そのまま</td>
  </tr>
  <tr>
    <td> PitchSatisfactionScore </td> <td>売り込みの満足度スコア</td> <td>そのまま</td>
  </tr>
  <tr>
    <td> Designation </td> <td> 現在の組織における顧客の指定</td> <td>文字表記の補正・統一</td>
  </tr>
  <tr>
    <td> MonthlyIncome </td> <td>顧客の月収</td> <td>文字表記の削除と変更をし整数表記へ</td>
  </tr>
  <tr>
    <td> customer_info </td> <td> 顧客の情報のメモ(婚姻状況や車の有無、旅行の子どもの同伴の有無について記載されている)</td> <td>①子供の数、②車所持の有無、③既婚、離婚、独身と未婚に分けて、いずれもLabelEncoding</td>
  </tr>
  <tr>
    <td> ProdTaken </td> <td> 目的変数 </td> <td> なし </td>
  </tr>
  
</table>

### 欠損値処理について
下記のようなものを試して、複数の特徴量を作成しました。
・LightGBMで予測してその予測値で補完。(変数選択は、予測値補完をしたい特徴量以外は全て同じものとして、ハイパーパラメータも同じにして実行した）
・主に中央値で補完。
・欠損値であることを残すため-1として補完
・0として補完。

:::note info
AgeやMonthyIncomeなどの連続値にはLightGBMで予測してその予測値で補完を採用していましたが、ソースコード含め、チャレンジ期間後半では中央値補完がメインです。そもそもLightGBMでの予測値による補完は、特徴量の数が増えるほどその予測値自体が変わる・変えられる可能性からあまり再現性がないよなあと思い至り最終的には不採用にしました。
:::



### モデル
・試したモデル:LightGBM,CatBoost,XGBoost,LDA,ロジスティック回帰、ニューラルネット
・一番良かった(最終的に採用した）モデル：LightGBM
・アンサンブル（スタッキング）：LightGBM、ニューラルネット

### 特徴量作成の一部(特徴量選択したものから抜粋）

<table>
  <caption>特徴量作成及び前処理後の変更</caption>
  <thead>
    <tr>
      <th> 特徴量ヘッダ名称 </th> <th>説明</th> <th>内容・補足</th> <th> 理由や仮説</th>
    </tr>
  </thead>
  <tr>
    <td> Age_Annual </td> <td>年齢×世帯年収</td> <td>年収に対する世帯収入という特徴量を作り掛け合わせ</td> <td> 年齢と世帯収入の関係性の強調</td>
  </tr>
  <tr>
    <td> CityTier_reverse </td> <td>都市層(1>2>3)</td> <td> 都市層(3>2>1)へ変換</td> <td>　都市層の規模を生かす為rank化 </td>
  </tr>
  <tr>
    <td> Occupation_rank </td> <td>顧客のご職業:中小、会社員、大企業</td> <td> 職業の規模を生かす為rank化 </td> <td> lgbm以外でもint型として扱う為 </td>
  </tr>
  <tr>
    <td> NumberOfFollowups_max </td> <td> セールストーク後に営業担当者が行ったフォローアップの合計数</td> <td>100以上を元の最大値＋1</td> <td>外れ値を新たな最大値として。Nanは0として扱う。</td>
  </tr>
  <tr>
    <td> NumberOfTrips_year </td> <td>顧客の年間旅行数</td> <td>年間の旅行回数で統一しNanは０扱い。</td> <td>旅行回数０もあり得るとした</td>
  </tr>
    <td> Product_Score </td> <td>売り込み商品のランク×売り込みの満足度</td> <td>表記揺れ等を統一後に計算</td> <td>商品ランクが良い程満足度も高く成約率も高いのでは</td>
  </tr>
  <tr>
    <td>time_of_follow_new <td>営業担当者のフォローアップ回数×顧客への売込み時間</td> <td> 交互作用による営業力の算出</td> <td>営業力が高い程成約率が上がるのでは</td>
  </tr>
  <tr>
    <td> Designation_rank </td> <td> 現在の組織における顧客の指定</td> <td>各意味合いに沿ってrank化した数値へ</td> <td> 可読性の向上、交互作用等に使う為</td> 
  </tr>
  <tr>
    <td> M_income_transformed </td> <td>顧客の月収</td> <td> Nanを予測値で変換したものをRANKGAUSS</td> <td>右裾が長い為標準化措置。決定技系以外で使用予定だった</td>
  </tr>
  <tr>
    <td> RankSatisfaction </td> <td>売り込み満足度*顧客ランク</td> <td> 前処理したものを掛け合わせた</td> <td>満足度と顧客ランクが高いほど成約率も高いのでは</td>
  </tr>
  <tr>
    <td> travel_frequency </td> <td> 年齢に対する旅行頻度</td> <td>年間旅行人数/年齢</td> <td>年齢と旅行頻度の関係性の強調</td>
  </tr>
  <tr>
    <td> material_count </td> <td> 婚姻状況 </td> <td> 既婚、離婚、独身及び未婚に分けてLabelEncoding</td> <td>成約可否には婚姻状況の関連性があるのでは</td>
  </tr>
  <tr>
    <td> family </td> <td> 家族構成人数 </td> <td>結婚有無＋子供の数</td> <td>家族構成人数が成約率と関連性があるのでは</td>
  </tr>
  
</table>



### 特徴量選択
・基本は手動（主に各特徴量の相関を見ながら、さらに重要度を加味しながら行なった）


### ハイパーパラメータ
入賞者の解法を参考にさせて頂きました。学習させない方向で何度も試しました。
"learning_rate": 1,
"num_leaves": 32,
'lambda_l1': 1,
'lambda_l2': 30,
"min_data_in_leaf": 20,
"max_depth": 1,
"n_estimators": 5000,
"feature_fraction": 0.9,
"bagging_fraction": 0.8,
"bagging_freq": 1,
"subsample_freq": 2,
"subsample": 0.7,
"random_state": 123,
"importance_type": "gain",
"early_stopping_rounds": 30,


### 他に試したこと(あまり上手くいかない、もしくは、わからないで不採用）
・アンダーサンプリング・オーバーサンプリング(スタッキング用としては採用）
・アンサンブル単純平均、加重平均
・Boruta:BalancedRandomForestClassifier

### できなかったこと
・Optunaによるパラメータチューニング(時間が足りないというより待てない）
・グリッドサーチ等による特徴量選択（待ってられない、自分で選んでやる!となって不採用)
・スタッキング（結局時間が足りなくなって、最後の最後,やけくそでGeminiに手伝ってもらったけどよくわからないまま）
・キャリブレーション



https://github.com/mission8th/source_code

### 感想
一番モヤモヤしてたのが、手元のtrとcvスコアとLBのスコアが中々nearにならないことでした。後半はAUCだけでなく、precition,recall,F1スコアも見てみましたが、指標がイマイチ曖昧な感じのまま進めていました（最終的には手元のSubmitで一番いいスコアが紛れていましたが）。random_seedを変えるだけでブレるとは入賞者の振り返りで耳にしていましたが、過学習を抑えるようパラメータ設定したものよりもやや過学習気味にした方がスコアが上がったりする、特に最初はCatBoost推しで、手元だと随分よく見せるので期待がどんどん高まる割にLBとの乖離の差が縮まらず最終的に不採用としました。結局、LightGBMに落ち着きました。スコア的には銅メダル水準のものは作れたかもしれないが、リーダーボードの結果からも暫定評価と最終評価のスコアの差があまりないモデルの方が上位に多く見られ汎用性があると言えるのかなと思いました。

### 反省点
色々試せた分、風呂敷を広げすぎて自分が追いつけていけなかったかもしれません、例えば以下のような事。

・特徴量作成の中でカテゴリ型用とint型用とで複数作った結果、混同して把握しづらくなった。
・特徴量選択を手動だけで決めた事。明確な取捨選択の判断ができておらず作るだけ作った感もありました。
・特徴量の意味や試したことをマメに記録しておかなかったせいで、一度不採用にしたものも時間が経つと忘れてもう一度実行してしまったりした。
・環境管理やログ保存。今回の経験から非常に勉強になりました。

他にも、スコアにこだわりすぎて本質を見失うような瞬間があることに気付けたことは大きかったです。初学者の域を出ない人間がガチャを回すが如く手動でハイパーパラメータ設定にのめり込んでしまい、時間をかなり浪費してしまいました。餅は餅屋でさっさとOptunaで良かったのではとも思いました。データに向き合うはずがスコアに固執していくのはある種当然なのだけども、何のために検証があるのかという事も改めて感じました。
気軽に相談したり聞いたりできる環境や参考にできる調べ方なんかももう少し改善しないとGeminiやChatGPTだけではどうにも遠回りな感じもしました。


### 元コード再現のためにやったこと
特徴量選択の違いであることまではわかっていたのですが、完全再現には至らず。元々投稿していたファイルの中に最終評価に対するスコアが良いものが紛れていたので、どうせならと思い、それをベースに何回か投稿し直していました（SOTAはチャレンジ終了後も投稿できるので良かったです)。
いつまでも探索するわけにもいかないので、切り上げましたが、結果的に最終評価スコアに関しては割と安定的に改善させることができました。暫定評価のスコアを超えることはできていないのですが、未知データに対してより安定した性能を発揮できるモデルが構築できたかなと思います。暫定スコア0.8314→0.8299、最終評価スコア0.8403→0.8434と改善。


### まとめ
挙げればキリがないほど課題が浮き彫りになるなと改めて思いました。この辺りを生かしてまた次に進みます。作って失敗して、もう一度組み直して、壊して、組み直して、でも夢中になれる。勉強と改善をこれからも繰り返していくことになるなと思いました。メダルを最低一個獲るまでやめられません。　生成AIなどもそうですが、相変わらず実務と関係あればなあと感じます。あとNNはもっと使い倒したいです。