たかいとの備忘録

自然言語処理や機械学習のことについて学んだことを忘れないように書いていけたらと思います.

【PCDUA】第1回 国土交通省 地理空間情報データチャレンジの戦い方を考える

はじめに

第1回 国土交通省 地理空間情報データチャレンジに関しては,以下をご参考ください.

signate.jp

金融データ活用チャレンジでも実施していたように,今回もbaselineとなるようなnotebookを共有することができたらと思います.

キックオフイベントにも参加させてもらいましたが,コンペ開催の目的など,とてもポジティブなものであり,コンペが盛り上がるといいなと思っております.(記事投稿の理由も少しでもコンペの活性化に貢献することを意識しており,最低限の実装とコンペを進める上で手詰まりを避けるためのアイデアをいくつか書かせていただきました.)

当該コンペはディスカッション(フォーラム)がなく,金融データ活用チャレンジでもbaselineコードなどの共有が少なかったため,コードを共有することにしました. また,ルールに書いてはありませんが,signateのコンペはたいてい前処理,学習,予測の3つにコードを分けた実装が多いため,そちらを少し意識した書き方をしております.

コンペでの一番の楽しみである,特徴量作成,自身の工夫を加えるというところにぜひ時間を割いてもらい,少しでもデータ分析コンペの楽しさを実感してもらえたら幸いです.(コンペ期間中に何もできずに終わってしまったというのを回避してもらえたら幸いです!)

コンペは始まったばかりで,的外れな意見もあるかと思いますが,何かの参考になれば幸いです.

今回のコンペに取り組むにあたって

今回のコンペはシンプルな課題設計となっており,トライしやすいコンペだと感じました.

ただし,使えるデータは豊富なので,そのあたりを如何に活用していくかが重要かと思いますが,この記事で公開するのは,それらの扱い方ではなく,それらに集中するためのモデルの学習および推論部分の実装となります.

また,分析環境やツールも充実しておりますが,使い慣れていない環境は最初に時間をだいぶ使うと思うので,使うものをいくつか絞るといいのではないかと思います.

提供されている分析環境は魅力的ですが,コンペが終わると一般の人は使えなくなってしまうので,コンペが終わってからも使えるcolab環境のコードを公開させていただきます. 今後,データ分析を個人的にするときなどにも,ぜひ活用していただければと思います!

Baseline Notebook

Baseline Notebookを以下で共有します.

colab.research.google.com

「ファイル」から「ドライブにコピーを保存」を選んでもらい,コピーすればほとんどそのまま使えると思います. signateからコンペのデータをダウンロードし,自身のドライブに保存してください. また,データの置く場所によって,データへのパスなどを書き換えてもらえたらと思います.

上記のNotebookを使用することで,特徴量などの工夫をしなくてもリーダーボードで約10000程度のスコアが出ます.

ここからは,公開したものに関して簡単に紹介します.

①Preprocessing「前処理」

def Preprocessing(input_df: pd.DataFrame()) -> pd.DataFrame():
    # いろいろ特徴量作成を追加する
    output_df = input_df.copy()
    return output_df

関数「Preprocessing」の中に,特徴量作成をいろいろと追加してもらえたらと思います. ここにいろいろな工夫を加えるのがコンペの醍醐味の一つかと思いますので,今回共有したnotebookでは,何も特徴量作成は行っておりません.

②Learning「学習」

baselineとして,kaggleなどのコンペで使用率の高い決定木ベースの手法の実装を共有します.

細かいことは,公式ドキュメントなどを確認してもらえたら幸いです. 初心者の方は,u++さんの「初手LightGBM」をする7つの理由をまずは読むのがオススメです. 自身としては,高速,高い精度が見込める,特徴量のスケーリングが不要(決定木系モデルなので)の三点が特にポイントかと思います.

def lightgbm_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame):
    lgb_train = lgb.Dataset(x_train, y_train)
    lgb_valid = lgb.Dataset(x_valid, y_valid)

    model = lgb.train(
                params = CFG.regression_lgb_params,
                train_set = lgb_train,
                num_boost_round = CFG.num_boost_round,
                valid_sets = [lgb_train, lgb_valid],
                callbacks=[lgb.early_stopping(stopping_rounds=CFG.early_stopping_round, verbose=CFG.verbose),
                           lgb.log_evaluation(CFG.verbose),
                          ]
            )
    # Predict validation
    valid_pred = model.predict(x_valid)
    return model, valid_pred
def xgboost_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame):
    xgb_train = xgb.DMatrix(data=x_train, label=y_train)
    xgb_valid = xgb.DMatrix(data=x_valid, label=y_valid)
    model = xgb.train(
                CFG.regression_xgb_params,
                dtrain = xgb_train,
                num_boost_round = CFG.num_boost_round,
                evals = [(xgb_train, 'train'), (xgb_valid, 'eval')],
                early_stopping_rounds = CFG.early_stopping_round,
                verbose_eval = CFG.verbose
            )
    # Predict validation
    valid_pred = model.predict(xgb.DMatrix(x_valid))
    return model, valid_pred
def catboost_training(x_train: pd.DataFrame, y_train: pd.DataFrame, x_valid: pd.DataFrame, y_valid: pd.DataFrame):
    cat_train = Pool(data=x_train, label=y_train)
    cat_valid = Pool(data=x_valid, label=y_valid)
    model = CatBoostRegressor(**CFG.regression_cat_params)
    model.fit(cat_train,
              eval_set = [cat_valid],
              early_stopping_rounds = CFG.early_stopping_round,
              verbose = CFG.verbose,
              use_best_model = True)
    # Predict validation
    valid_pred = model.predict(x_valid)
    return model, valid_pred

def gradient_boosting_model_cv_training(method: str, train_df: pd.DataFrame, features: list):
    # Create a numpy array to store out of folds predictions
    oof_predictions = np.zeros(len(train_df))
    oof_fold = np.zeros(len(train_df))
    kfold = KFold(n_splits=CFG.n_folds) # , shuffle=True, random_state=CFG.seed)
    for fold, (train_index, valid_index) in enumerate(kfold.split(train_df)):
        print('-'*50)
        print(f'{method} training fold {fold+1}')

        x_train = train_df[features].iloc[train_index]
        y_train = train_df[CFG.target_col].iloc[train_index]
        x_valid = train_df[features].iloc[valid_index]
        y_valid = train_df[CFG.target_col].iloc[valid_index]
        if method == 'lightgbm':
            model, valid_pred = lightgbm_training(x_train, y_train, x_valid, y_valid)
        if method == 'xgboost':
            model, valid_pred = xgboost_training(x_train, y_train, x_valid, y_valid)
        if method == 'catboost':
            model, valid_pred = catboost_training(x_train, y_train, x_valid, y_valid)

        # Save best model
        pickle.dump(model, open(CFG.MODEL_DATA_PATH / f'{method}_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'wb'))
        # Add to out of folds array
        oof_predictions[valid_index] = valid_pred
        oof_fold[valid_index] = fold + 1
        del x_train, x_valid, y_train, y_valid, model, valid_pred
        gc.collect()

    # Compute out of folds metric
    score = np.sqrt(mean_squared_error(train_df[CFG.target_col], oof_predictions))
    print(f'{method} our out of folds CV rmse is {score}')
    # Create a dataframe to store out of folds predictions
    oof_df = pd.DataFrame({CFG.target_col: train_df[CFG.target_col], f'{method}_prediction': oof_predictions, 'fold': oof_fold})
    oof_df.to_csv(CFG.OOF_DATA_PATH / f'oof_{method}_seed{CFG.seed}_ver{CFG.VER}.csv', index = False)

def Learning(input_df: pd.DataFrame, features: list):
    for method in CFG.METHOD_LIST:
        gradient_boosting_model_cv_training(method, input_df, features)

モデルの学習は,学習データへの過学習を防ぐためにクロスバリデーションすることが多く,上記のコードも学習データを5分割しております. クロスバリデーションに関しては,交差検証(cross validation/クロスバリデーション)の種類を整理してみたなどが参考になるかと思います. 今回のデータは,学習データとテストデータが時系列で分割されているため,まず時系列で学習データを並び替え,それを均等に5分割することで,未知の期間に頑健なクロスバリデーション戦略となっております.

③Predicting「予測」

def lightgbm_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'lightgbm_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict(x_test)
        test_pred += pred
    return test_pred / CFG.n_folds
def xgboost_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'xgboost_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict(xgb.DMatrix(x_test))
        test_pred += pred
    return test_pred / CFG.n_folds

def catboost_inference(x_test: pd.DataFrame):
    test_pred = np.zeros(len(x_test))
    for fold in range(CFG.n_folds):
        model = pickle.load(open(CFG.MODEL_DATA_PATH / f'catboost_fold{fold + 1}_seed{CFG.seed}_ver{CFG.VER}.pkl', 'rb'))
        # Predict
        pred = model.predict(x_test)
        test_pred += pred
    return test_pred / CFG.n_folds

def gradient_boosting_model_inference(method: str, test_df: pd.DataFrame, features: list):
    x_test = test_df[features]
    if method == 'lightgbm':
        test_pred = lightgbm_inference(x_test)
    if method == 'xgboost':
        test_pred = xgboost_inference(x_test)
    if method == 'catboost':
        test_pred = catboost_inference(x_test)
    return test_pred

def Predicting(input_df: pd.DataFrame, features: list):
    output_df = input_df.copy()
    output_df['pred'] = 0
    for method in CFG.METHOD_LIST:
        output_df[f'{method}_pred'] = gradient_boosting_model_inference(method, input_df, features)
        output_df['pred'] += CFG.model_weight_dict[method] * output_df[f'{method}_pred']
    return output_df

第1回 国土交通省 地理空間情報データチャレンジの戦い方(工夫点)の案

ここからは,どのような工夫点が考えられるかをいくつか書いておきたいと思います.

類似コンペから工夫を考える

調べればわかりますが,賃料予測のコンペはこれまで何度も開催されております. データは違えど,賃料に影響を与えている要因の中には普遍的なものが多くあることが想定されるため,過去コンペの上位解法を読むことは,大きなヒントになるかと思います. なお,以下に紹介するコンペに自身は参加していないため,細かい条件などは把握していないことをご了承ください.

・【SIGNATE】マイナビ × SIGNATE Student Cup 2019: 賃貸物件の家賃予測 signate.jp

上位解法者のレポートが入賞者レポートに掲載されております.(kumaさんやcharmさんがまだ学生ですね.大村さんはリサーチのイメージが強いので,こういったコンペに参加していたのは驚きでした.)

マイナビ × SIGNATE Student Cup 2019: 賃貸物件の家賃予測 | SIGNATE - Data Science Competition

イデア賞も,今回のアイデア部門の参考になりそうですね. u++さんがまとめ記事も書いてくれています.

「マイナビ × SIGNATE Student Cup 2019: 賃貸物件の家賃予測」まとめ - u++の備忘録

・【ProbSpace】 不動産取引価格予測 comp.probspace.com こちらも国内のプラットフォームで開催されたコンペになります. 優勝者はトピックで解法を共有してくれているので,参考になるかと思います.

不動産取引価格予測1位解法 | ProbSpace

・【kaggle】 House Prices - Advanced Regression Techniques アメリカ合州国アイオワ州エイムズ市の戸建て住宅の価格を予測するコンペです. 日本語で書かれた記事もあるので参考までに.

【Kaggle House Prices 】初心者でもできる! AIで住宅価格を予測する|ESTYLE(エスタイル)

【kaggle】住宅価格予測コンペで機械学習モデル比較 #Python - Qiita

それ以外にも,家賃予測に関する記事も公開されております.

以下は,Kaggle Grandmasterのpaoさんが書かれた記事で,モデリング部門はもちろんのこと,アイデア部門の参考になりそうですね.

qiita.com

データ分析

今回,時系列でデータを分割しておりますが,最初のfoldのvalidationデータによる評価があまり良くないことがわかります. こういった情報からも,いろいろと分析することでわかることがあるかと思います. コンペの規約上,データを可視化した結果などをブログ記事に載せていいかわからなかったため,そういったEDAはbaselineで共有していませんが,データの分析はモデリングにおいても非常に重要となります.

特徴量作成

こういったデータが豊富なコンペにおいては,特徴量作成が非常に重要になってきます. そもそもこのnotebookでは,数値データのみを扱っており,全てのデータを使用していないので,まずはこのあたりから始めてみてはいかがでしょうか.

その他にも,四則演算したり集計したりと,いろいろな特徴量が考えられるかと思いますので,ぜひいろいろな仮説を立てて,オリジナルの特徴量を考えてスコアを伸ばしてもらえたらと思います.

また,共有した特徴量の重要度を確認しながら,どの特徴量に着目して,特徴量作成を行ったりしていくと効率がいいかもしれません. 共有した以下のコードを実行すると重要な特徴量がソートされて表示されます.

model = pickle.load(open(CFG.MODEL_DATA_PATH / f'lightgbm_fold1_seed42_ver1.pkl', 'rb'))
importance_df = pd.DataFrame(model.feature_importance(), index=features, columns=['importance'])
importance_df['importance'] = importance_df['importance'] / np.sum(importance_df['importance'])
importance_df.sort_values('importance', ascending=False)

「house_area(建物面積/専有面積)」や「unit_area(専有面積)」,「post1(郵便番号1)」,「parking_money(駐車場料金)」などが重要であることがわかります.

モデルの検討

今回のベースラインでは決定木ベースのモデルとして,lightgbm,xgbboost,catboostを紹介しました.

それ以外にもロジスティック回帰やSVMなど,いろいろモデルを試してみる価値があるかもしれません. 複数のモデルの結果は,スコアを上げる方法の1つであるアンサンブルで,強力な武器となる可能性があるので,時間の許す範囲でいろいろ試してみるべきかと思います.(PyCaretとか便利かと思います.)

また,深層学習系統のモデルも余裕があれば試す価値があると思います.

ハイパーパラメータチューニング

自身はあまり重視していませんが,やる価値はもちろんあります. ただ,他の工夫を優先した方が,大幅なスコア上昇が見込めるかと思いますので,時間が余ったら程度にいつもしております.

後処理

後処理は,いろいろなコンペでスコアの底上げに貢献しています.

外れ値は評価指標に大きく影響を与えるため,そういったデータを検出するモデルを作成し,ルールベースで異なる後処理をするなど,後処理もいろいろと工夫の余地があるかと思います!

おわりに

コンペ開催期間中の質問はコンペ専用slackの記事へのリンクを貼り付けたスレッドにしていただければ,返信できるかと思います.