Nishika 中古マンション価格予測コンペ 2023夏の部 3位解法

nishika 中古マンションコンペ 2023 夏の部で362人中3位に入賞したので、ソリューションを共有します。
過学習によるCVとLBの乖離に苦しめられ、大量に作った特徴量があまり効果がないことが多く、思ったよりかなり難しいコンペでした。
https://competition.nishika.com/competitions/mansion_2023summer/ranking


![nishika.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252024/11f23364-eeb5-b5ed-a465-c10b371a5f62.png)

## コンペ概要

- 物件の情報（築年数、最寄駅、構造など）から中古マンションの価格を予測するコンペ
    - train:2006～2020年 / test:2021年
    - 価格は常用対数化されている。実際の値幅は450円～86億円
- 定期開催（今回で8回目）最近はルール変更に伴い競争が激化
    - ～2022夏　上位１０名入賞
    - 2023冬　　上位５名のみ入賞
    - 2023春～　上位３名のみ入賞
- 評価関数：**MAE**
- シェイク：ほぼしない ⇒ trust CVではなく**trust LB 戦略**
    - ただし過学習すると当然シェイクダウンする
- 過去のソリューションやノウハウ：Qiitaの記事やコンペのトピック欄などを参考にした
    - NRIはプレスリリースにソリューションを記載していて興味深かった(https://www.nri-digital.jp/news/20221115-11897/)

## ソリューション概要
![スクリーンショット 2023-10-12 22.37.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252024/c4450820-24b9-dec1-9ffe-21da1756bde9.png)




- 損失関数２通り、目的変数２通り、合計４パターンの学習を10foldで実施しアンサンブル
    - 損失関数：**Lq loss**　２パターン（q=1.3 / 1.4）
    - 目的変数：**①物件の価格　②物件の単位面積当たりの価格**（平米単価）　２パターン
        - ②の場合は、平米単価を予測して後処理で面積をかけて最終予測にする
    - アンサンブル：損失関数毎に目的変数①：②＝1:1でアンサンブルした後、さらにLq1.3:Lq1.4 = 7:3でアンサンブル
    - Catboost
    - 「市区町村」でstratified k-fold
        - train内の市区町村約650件に対し、test.csvに含まれる市区町村が522と多かったため
        - 過学習してしまったことを考えると、他のfoldの切り方も試せばよかった

## 効いたもの

    
#### **① Lq Loss**
```math
L(y, p) = |y - p|^q
```

このコンペはlog7近辺の価格データが多いため、コンペの評価関数であるMAEを損失関数にすると、log7近辺の物件の精度は良くなる一方、極端に価格の大きい/小さい物件の予測精度が悪くなってしまう。
Lq lossはMAEとMSEの両方の特徴を持つ損失関数で、q=1のときMAE、q=2のときMSEと等しくなる。すなわちqを1より増加させることで、極端な価格の物件の予測で生じる大きな予測誤差に対して、MAEより大きなペナルティを与えることができる。今回はこのlossに気づけたおかげで入賞圏内に入ることができた。
以下の通り簡単に設定できる。

    
```python:Lq Lossの設定例
model = CatBoostRegressor(
    iterations=5000, 
    learning_rate=0.030, 
    depth=10, 
    use_best_model=True,
    eval_metric='MAE', 
    od_type='Iter',
    od_wait=200,
    loss_function='Lq:q=1.3')
```

※ちなみに、MAEで学習させた場合とLq lossをq=1で学習させた場合では、なぜかモデルのサイズが数倍違い、前者は数ギガなのに対し後者は数百メガバイトとかなり軽量になる。

このコンペで高スコアを取るには極端な価格の物件への対応が必要で、過去のソリューションにはhuber lossなどが登場していた。その他の損失関数については以下の記事が詳しい：
「損失関数のまとめ (Huber,Log-Cosh,Poisson,CustomLoss,MAE,MSE)」@Hatomugi
https://qiita.com/Hatomugi/items/d00c1a7df07e0e3925a8



####  **② もっとも効いた特徴量: "面積-築年数"**
自分がマンションを購入する立場に立って考えた時に、「新しくて広い家かどうか」を評価する指標として導入した。この”面積-築年数”は、物件が新しくて広い家の場合に大きな値を取る。
以下に示す通り、 feature importance Top10 では、“面積-築年数”が最も効いている結果になった。（図中では“面積-X”と表記）みんなどうせ住むなら新しくて広い家がいいよね。。。
![スクリーンショット 2023-10-11 9.11.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252024/b99a4684-fb0f-d66c-8b6e-ac8e881f1fce.png)



そのほか、以下の特徴量エンジニアリングを実施。
- 築年数変換
- 区のランク付け（都心６区＋横浜・新都心三区）
- 市区町村毎の平米単価
- 「用途」の最頻値での補完
- 用途目的一致フラグ
- 旧耐震フラグ
- 駅徒歩分数のランク付け
- 間取りのランク付け
- 一部屋あたりの面積
- 取引時築年数
- target encoding

## 効かなかったもの

- 建蔽率と容積率の比
- pseudo labeling
    - 一定以下のスコアでは効果があったが、途中で頭打ちになった
- 集約特徴量全般
    - ほかの入賞者のソリューションではお勧めされていたが、過学習してしまった
    - https://www.smartbowwow.com/2022/07/nishika-2022-3rd-prize-solution.html など
    - 実装にはPFN製のxfeatを使用。使い勝手はとても良かったので今後も使っていきたい。
- 駅の乗降客数データ
- 駅毎の取引回数・地区毎の取引回数
- 市と区の分離
- 対数変換
- huber loss(th=0.06)
- n_fold=15
- 築年数や面積の補間（kNNInputerを使用）
- 外れ値の除去
    - 面積や築年数の上位・下位100件を削っただけでLBスコアが大幅に悪化
    ⇒より外れ値を重視する損失関数を使うきっかけに
    

## あきらめたもの・時間的に試せなかったもの

- 住所の名寄せ
    - 自由が丘、自由ヶ丘、自由ケ丘　など
- 最寄り駅の名寄せ
    - JR大阪＝阪急梅田　小川町＝新御茶ノ水＝淡路町　など　実質同じ駅が数多く存在する
    - 神戸の三宮駅周辺
        - 阪急と阪神は「神戸三宮」、JR西日本は「三ノ宮」だが、2014年以前は阪神は「三宮」だった
- 低価格物件についての特徴量エンジニアリング
    - 都市計画の計画区域外フラグなど
    - 犯罪率データなど
- NNとのアンサンブル
    - kaggle Bookで推奨されていた方法

## 感想・残った疑問

- 複数の目的変数や損失関数を組み合わせ、過学習しにくいソリューションを作ることができた一方、特徴量エンジニアリングがうまくいかず結局モデリング勝負のソリューションになってしまった。
- 評価関数を損失関数に使わない、購買者が求めるものをうまく定式化するなど、そこそこ工夫して取り組めた。
- 上位３名しか入賞できないのは厳しい。枠はもう少し増やしてほしいのが本音。
- lightGBMとcatboostの優劣については過去のトピックでは意見が二分しており、今回の自分のケースではlightgbmは過学習してしまい全く使い物にならなかった。また、CVとLBを対応させる方法は最後まで見つからなかった。
- あるソリューションでスコアに効果ありとされた特徴量が全く効かないことがあり、特にターゲットエンコーディング、集約特徴量、pseudo labeling、建蔽率と容積率の比などは効果に差がありそう。
- 複数回参加者の強さの秘密は何か？過去１年間の優勝者は必ず複数回参戦者で占められており、初参戦者には不利なコンペだった。



今後も定期開催されるとのことなので、コードやパラメータなどの詳細は伏せますが、テーブルコンペ初心者には大変ためになるコンペでした。最後までお読みいただきありがとうございました。