📑

SIGNATE Career Up Challenge 2023の24位解法

2023/08/29に公開

他の人の解法を見ようと思ったのに、あんまり解法が見当たらなかったので書きました。

上位解法は表彰式に参加して確認しましょう。

タスク

以下のような中古車販売のデータが与えられるので値段(price)を当てるものです。

走行距離 車種 price
30000 トヨタ 100
10000 日産 Null

評価指標

\text{MAPE} = \dfrac{100}{n}\displaystyle\sum_{i=1}^n|\dfrac{y_i-\hat{y_i}}{y_i}|

評価指標がMAPEだったのがこのコンペで難しい部分であり、面白い部分でもあったかもしれません。3000に60000を予想すると2000%となりペナルティが非常に大きい一方60000に3000だと95%とペナルティがそこまで大きいわけではないという特徴があります。

特徴量生成

このデータには地域と州があったのですが、州にはいくつかnullがありました。なので地域から州のnullを埋めることをしました。特徴量生成で頑張ったのはここだけです。そして効果は少ししかなかったです。

他はyearとmanufacturerとsizeの表記ゆれを直したこととsizeがsub-compact,compact,mid-size,full-sizeだったので0,1,2,3に置き換えました。

all_data['year'] = all_data['year'].apply(lambda x: int(str(x).replace('29', '19')) if str(x)[:2]=='29' else int(str(x).replace('30', '20')))
all_data['manufacturer'] = all_data['manufacturer'].apply(lambda x: unicodedata.normalize('NFKC', x).lower().replace('а','a').replace('α','a').replace('о','o').replace('ᴄ','c'))
all_data['size'] = all_data['size'].apply(lambda x: x.replace('ー','−').replace('-','−'))
def sizetoint(x):
    if x == 'sub−compact':
        return 0
    elif x == 'compact':
        return 1
    elif x == 'mid−size':
        return 2
    elif x == 'full−size':
        return 3

all_data['size'] = all_data['size'].apply(sizetoint)

地域から州情報の穴埋めですが、geopyを使って住所の正規化をしました。

region_name = all_data["region"].unique()
re_df = pd.DataFrame(region_name,columns=['region'])
address = []
for num,i in enumerate(region_name):
    time.sleep(0.1)
    # アメリカの地名なので語尾にUnited Statesを付ける
    location = geolocator.geocode(i + ' United States',timeout=10)
    if location is None:
        address.append(np.nan)
        continue
    address.append(location.address)
re_df['address'] = address

stateの方も同じようにして州のaddressがマッチすれば穴埋めみたいな感じです。
2つ州の住所が間違っていたので手動で修正したのと一部regionが取れなかったので手動で州を書きました。

モデル部分

なんとなくここの部分が大事だった気がします。僕は今回

一段階目  logを取った値段を予測するモデル。予測した後で指数変換して元に戻す
二段階目  一段階目の予測値と実際の値段でAPEを計算して、それを当てるモデル
その後、一段階目の予測値を(1+二段階目の予測値)で割る

という二段構造でした。出来た感想としてはなんだこの意味不明なモデルはという気持ちです。

一段階目は

Kfold = KFold(n_splits=5, shuffle=True, random_state=42)
oof = np.zeros(len(train))
y_pred = np.zeros(len(test))
train["log_price"] = np.log10(np.log(x))
for fold, (train_idx, valid_idx) in enumerate(Kfold.split(train, train['log_price'])):
  model.fit()
    model.predict()
oof = np.exp(10 ** oof)
y_pred = np.exp(10 ** y_pred)   

二段階目は

# APEより大小関係を考慮した数値のほうがいい気はするが、APE使ったものが精度は良かった
train['pred_price'] =  oof
train['ape'] = (abs(train['price'] - train['pred_price'])/train['price'])
test['pred_price'] = y_pred
test['ape'] = (abs(test['price'] - test['pred_price'])/test['price']) 
oof2 = np.zeros(len(train))
y_pred2 = np.zeros(len(test))
for fold, (train_idx, valid_idx) in enumerate(Kfold.split(train, train['ape'])):
  model.fit()
    model.predict()
oof =  oof/(1+oof2)
y_pred = y_pred/(1+y_pred2)

後処理とアンサンブル

後処理はoofを使って0~20000と20000~40000でmapeが最適になるように定数倍しました。
アンサンブルはlogのやり方を変えて3つ混ぜました。log10(log)とlog10(log2)とlog2(log)
モデルはlightgbmとxgboostとcatboostを混ぜました。

何がスコアに影響したか

二段階モデル+後処理>>後処理のみ>>予測値の対数化>>>>特徴量生成
くらいだと思います

反省点

二段階モデルのやり方はもっと工夫の仕方があったかなと思いました。その辺が上位との差だったかもしれません。

最後に

こういうテーブルデータのコンペは少なくなっていると思うので、触れてよかったかなと思います。SIGNATEさんコンペを開いていただきありがとうございました。

あと上位解法を表彰式に参加して確認しましょう。

Discussion