{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":50160,"databundleVersionId":7921029,"sourceType":"competition"}],"dockerImageVersionId":30635,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"このNotebookは[Home Credit 2024 Starter Notebook](https://www.kaggle.com/code/jetakow/home-credit-2024-starter-notebook)を参考に作っています。よろしければ本家の方もupvoteしていただけると幸いです。\n\n---\n\n## コンペの概要\n\nこのコンペティションの目的は、融資先のデフォルト (返済不能) リスクを予測することです。評価は、時間的に安定したソリューションが優先されます。\n\n参加することで、消費者金融業者に、潜在顧客のデフォルトリスクをより信頼性が高く、長期間にわたって評価する方法を提供できる可能性があります。\n\nクレジットヒストリーの未記入は、若年層であることや現金決済を好むなど、さまざまな理由が考えられます。従来のデータがないと、クレジットヒストリーがほとんどない人は融資を断られる可能性が高くなります。消費者金融業者は、融資を返済できる顧客とそうでない顧客を正確に判断する必要があり、データが重要です。データサイエンスが返済能力の予測を向上させるのに役立つなら、融資は恩恵を受ける可能性が最も高い人にさらに利用しやすくなるかもしれません。\n\n現在、消費者金融業者は融資リスクを予測するために、さまざまな統計手法や機械学習手法を用いています。これらのモデルは、一般的にスコアリングカードと呼ばれます。現実世界では、顧客の行動は常に変化するため、すべてのスコアリングカードは定期的に更新する必要があります。この更新には時間がかかります。スコアリングカードの将来的な安定性は非常に重要です。パフォーマンスが突然低下すると、平均的によりリスクの高い顧客に融資が発行されることになるからです。問題の中心は、融資者が最初の返済期日が観察されるよりも早く潜在的な問題を発見できないことです。スコアリングカードを再開発、検証、実装するのに時間がかかるため、安定性は非常に望ましいものです。モデルの安定性とパフォーマンスはトレードオフであり、導入する前にバランスを取ることが必要です。\n\n潜在顧客のデフォルトリスク評価を支援する皆様の取り組みは、消費者金融業者がより多くの融資申請を受け入れることを可能にします。これにより、これまでクレジットヒストリ不足のために融資を断られていた人々の生活を向上させることができるかもしれません。\n\n\n## データセットの説明\n\nこのコンペティションでは、融資先の内部情報と外部情報に基づいて、デフォルト (返済不能) を予測します。スコアリングは、予測のAUC (Area Under the Curve) を評価するだけでなく、テストセットのデータ範囲全体における予測の安定性も考慮したカスタムメトリックを使用して行われます。このメトリックをより理解するために、**評価タブ**を参照してください。\n\n### テーブルの説明\n\nこのデータセットは、さまざまなデータソースを活用し、データセットの準備中に異なる集計レベルが使用された結果として、多数のテーブルで構成されています。\n\n**注意:** 以下のすべてのファイルは、`.csv` 形式と `.parquet` 形式の両方で存在します。\n\n#### 基本テーブル\n\n基本テーブルは、観測値と `case_id` に関する基本情報を格納します。`case_id` は、各観測値の一意の識別子であり、他のテーブルを基本テーブルに結合するために使用する必要があります。\n\n**訓練ファイル:**\n\n* train_base.csv\n\n**テストファイル:**\n\n* test_base.csv (注意: 非公開の `test_base.csv` は、`train_base.csv` の `case_id` の値の約 90% を含んでいます)\n\n#### その他のテーブル\n\n残りのテーブルは、`case_id` に関連する追加情報を提供します。テーブル名は次の形式で構成されています。\n\n* `\\<テーブル名\\>_\\[depth\\]_\\[データソースの種類\\]`\n\n- `depth`: データの階層を示します。\n    - `depth=0`: 特定の `case_id` に直接結びつく静的な特徴量です。\n    - `depth=1`: 各 `case_id` は `num_group1` でインデックス付けされた履歴レコードを持ちます。\n    - `depth=2`: 各 `case_id` は、`num_group1` と `num_group2` の両方でインデックス付けされた履歴レコードを持ちます。\n- `データソースの種類`: データのソースを示します。\n    - `internal`: 内部データソース\n    - `external`: 外部データソース\n\n**depthごとの説明**\n\n* `depth=0` - これらは、特定の `case_id` に直接結びつく静的な特徴量です。\n* `depth=1` - 各 `case_id` は、`num_group1` でインデックス付けされた履歴レコードを持ちます。\n* `depth=2` - 各 `case_id` は、`num_group1` と `num_group2` の両方でインデックス付けされた履歴レコードを持ちます。\n\n**列の説明**\n\n#### 特殊な列\n\n* `case_id` - 各信用事象の一意の識別子です。この ID を使って、関連するテーブルを基本テーブルに結合する必要があります。\n* `date_decision` - ローンの承認に関する決定が行われた日付を表します。\n* `WEEK_NUM` - 集計に使用される週番号です。テストサンプルでは、`WEEK_NUM` は、訓練データの最後の `WEEK_NUM` の値から連番で続きます。\n* `MONTH` - この列は月を表し、集計に使用されます。\n* `target` - 特定の信用事象 (ローン) で融資者がデフォルトしたかどうかをもとに、一定期間後に決定されたターゲット値です。\n* `num_group1` - `depth=1` と `depth=2` の両方のテーブルで、`case_id` の履歴レコードに使用されるインデックス列です。\n* `num_group2` - `depth=2` テーブルの `case_id` の履歴レコードに対する 2 番目のインデックス列です。`num_group1` と `num_group2` の順序は重要であり、特徴量定義で説明されます。\n\n#### 予測変数\n\nテーブル内の残りのすべての生データ列は、予測変数として機能します。それらの定義は、ファイル `feature_definitions.csv` にあります。\n\n* `depth=0` のテーブルの場合、予測変数は直接特徴量として使用できます。\n* ただし、`depth>0` のテーブルの場合、各 `case_id` に関連する履歴レコードを単一の機能に凝縮する集計関数を使用する必要があるかもしれません。\n\n`num_group1` または `num_group2` が人インデックスを表す場合 (これは予測変数の定義で明らかです)、インデックス 0 は特別な意味を持ちます。`num_groupN=0` の場合は、申請人 (ローンを申請した人) を表します。\n\n## 予測変数の変換\n\n様々な予測変数が変換されました。そのため、変換のグループごとに以下の表記方法を使用しています。\n\n* **P** - 過去の期日超過日数 (Days Past Due, DPD) の変換\n* **M** - カテゴリのマスキング\n* **A** - 金額の変換\n* **D** - 日付の変換\n* **T** - 未指定の変換\n* **L** - 未指定の変換\n\n変換グループ内の変換は、予測変数名の末尾に大文字で示されています (例: `maxdbddpdtollast6m_4187119P`)。 これにより、予測変数の操作が簡素化されることを願っています。\n\n**Edits**:\n\n* `pmts_month_158T` は有効な契約を示します。\n* `pmts_month_706T` は終了した契約を示します。\n\n\n# Example Notebook\n\nこのノートブックは、Home Credit Kaggle コンペティションのサンプルです。このコンペティションの目的は、融資先のデフォルト (返済不能) リスクを予測することです。\n\nこのコンペティションと 最初のコンペティション: [https://www.kaggle.com/c/home-credit-default-risk](https://www.kaggle.com/c/home-credit-default-risk) との主な違いは、提出されたモデルが将来の成績を考慮して評価される点にあります。パフォーマンスの低下はペナルティの対象となります。目標は、安定していて将来も良好な成績を維持するモデルを作成することです。\n\nこのノートブックでは、以下のようなことを解説します。\n\n* データの読み込み\n* Polars ライブラリを使用した結合処理: Polars は Rust言語で実装された DataFrame ライブラリで、非常に高速でメモリ効率に優れています。\n* 集約特徴量の作成\n* LightGBM モデルの訓練\n* 提出用テーブルの作成\n\n\n## Load the data","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"import polars as pl\nimport numpy as np\nimport pandas as pd\nimport lightgbm as lgb\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score \n\ndataPath = \"/kaggle/input/home-credit-credit-risk-model-stability/\"","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:23.311452Z","iopub.execute_input":"2024-03-16T07:28:23.311873Z","iopub.status.idle":"2024-03-16T07:28:28.015706Z","shell.execute_reply.started":"2024-03-16T07:28:23.311836Z","shell.execute_reply":"2024-03-16T07:28:28.014955Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**1. `set_table_dtypes` 関数**\n\n- この関数は、データフレーム `df` の列のデータ型を設定します。\n- 列名の最後の文字によって、データ型を決定します。\n- 列名の最後の文字が \"P\" または \"A\" の場合、その列を浮動小数点型 (`pl.Float64`) に設定します。\n\n**2. `convert_strings` 関数**\n\n- この関数は、データフレーム `df` の文字列型の列をカテゴリ型に変換します。\n- 列のデータ型が 'object' または 'string' の場合、その列を文字列型にしてからカテゴリ型に変換します。\n- 現在のカテゴリ (`current_categories`) に \"Unknown\" カテゴリを追加して、新しいカテゴリ型 (`new_dtype`) を作成します。\n- 列を新しいカテゴリ型 (`new_dtype`) に変換します。\n","metadata":{}},{"cell_type":"code","source":"def set_table_dtypes(df: pl.DataFrame) -> pl.DataFrame:\n    # implement here all desired dtypes for tables\n    # the following is just an example\n    for col in df.columns:\n        # last letter of column name will help you determine the type\n        if col[-1] in (\"P\", \"A\"):\n            df = df.with_columns(pl.col(col).cast(pl.Float64).alias(col))\n\n    return df\n\ndef convert_strings(df: pd.DataFrame) -> pd.DataFrame:\n    for col in df.columns:  \n        if df[col].dtype.name in ['object', 'string']:\n            df[col] = df[col].astype(\"string\").astype('category')\n            current_categories = df[col].cat.categories\n            new_categories = current_categories.to_list() + [\"Unknown\"]\n            new_dtype = pd.CategoricalDtype(categories=new_categories, ordered=True)\n            df[col] = df[col].astype(new_dtype)\n    return df","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:41.806303Z","iopub.execute_input":"2024-03-16T07:28:41.807063Z","iopub.status.idle":"2024-03-16T07:28:41.814899Z","shell.execute_reply.started":"2024-03-16T07:28:41.807030Z","shell.execute_reply":"2024-03-16T07:28:41.813861Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**コンペティションの訓練データを読み込みます。**\n\n**1. 基本情報 (train_basetable)**\n\n* `pl.read_csv(dataPath + \"csv_files/train/train_base.csv\")` は、指定されたパスから CSV ファイルを読み込んで、データフレーム `train_basetable` に格納します。\n* このデータフレームには、おそらく融資先の基本情報 (氏名、住所、ローン種別など) が含まれていると考えられます。\n\n**2. 静的特徴量 (train_static)**\n\n* `pl.concat` 関数を使って、複数の CSV ファイルを読み込んで結合しています。\n    * 最初のカッコ内 (`[ ... ]`) は、結合するデータフレームのリストです。\n    * `pl.read_csv` を使って、2つの CSV ファイル (`train_static_0_0.csv` と `train_static_0_1.csv`) を読み込みます。\n    * `.pipe(set_table_dtypes)` は、読み込んだデータフレームに対して `set_table_dtypes` 関数を実行し、データ型を設定しています。\n* `how=\"vertical_relaxed\"` は、結合の方法を指定しています。\n    * この場合、\"vertical\" は行方向に結合することを意味します (列は結合されません)。\n    * \"relaxed\" は、結合するデータフレームに欠損値があっても結合処理を継続することを意味します。\n* 結合されたデータフレームは `train_static` に格納されます。\n* このデータフレームには、おそらく融資先の過去の情報や属性 (過去のローン履歴、信用スコアなど) が含まれていると考えられます。\n\n**3. その他の訓練データ**\n\n* 同様の方法で、残りの 3 つの CSV ファイルも読み込んでいます。\n    * `train_static_cb_0.csv` は、外部データソース (信用情報機関など) から取得した静的な情報\n    * `train_person_1.csv` は、融資先の属性情報 (年齢、職業など) \n    * `train_credit_bureau_b_2.csv` は、別の信用情報機関からの情報\n* 読み込んだデータフレームには、それぞれ `train_static_cb`、`train_person_1`、`train_credit_bureau_b_2` という名前が付けられています。\n* これらのデータフレームは、分析に使用する特徴量を含むと考えられます。\n\nテストデータも同様に読み込んでいきます","metadata":{}},{"cell_type":"code","source":"train_basetable = pl.read_csv(dataPath + \"csv_files/train/train_base.csv\")\ntrain_static = pl.concat(\n    [\n        pl.read_csv(dataPath + \"csv_files/train/train_static_0_0.csv\").pipe(set_table_dtypes),\n        pl.read_csv(dataPath + \"csv_files/train/train_static_0_1.csv\").pipe(set_table_dtypes),\n    ],\n    how=\"vertical_relaxed\",\n)\ntrain_static_cb = pl.read_csv(dataPath + \"csv_files/train/train_static_cb_0.csv\").pipe(set_table_dtypes)\ntrain_person_1 = pl.read_csv(dataPath + \"csv_files/train/train_person_1.csv\").pipe(set_table_dtypes) \ntrain_credit_bureau_b_2 = pl.read_csv(dataPath + \"csv_files/train/train_credit_bureau_b_2.csv\").pipe(set_table_dtypes) ","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:43.742634Z","iopub.execute_input":"2024-03-16T07:28:43.743032Z","iopub.status.idle":"2024-03-16T07:28:58.522262Z","shell.execute_reply.started":"2024-03-16T07:28:43.743003Z","shell.execute_reply":"2024-03-16T07:28:58.521406Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_basetable = pl.read_csv(dataPath + \"csv_files/test/test_base.csv\")\ntest_static = pl.concat(\n    [\n        pl.read_csv(dataPath + \"csv_files/test/test_static_0_0.csv\").pipe(set_table_dtypes),\n        pl.read_csv(dataPath + \"csv_files/test/test_static_0_1.csv\").pipe(set_table_dtypes),\n        pl.read_csv(dataPath + \"csv_files/test/test_static_0_2.csv\").pipe(set_table_dtypes),\n    ],\n    how=\"vertical_relaxed\",\n)\ntest_static_cb = pl.read_csv(dataPath + \"csv_files/test/test_static_cb_0.csv\").pipe(set_table_dtypes)\ntest_person_1 = pl.read_csv(dataPath + \"csv_files/test/test_person_1.csv\").pipe(set_table_dtypes) \ntest_credit_bureau_b_2 = pl.read_csv(dataPath + \"csv_files/test/test_credit_bureau_b_2.csv\").pipe(set_table_dtypes) ","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:58.524114Z","iopub.execute_input":"2024-03-16T07:28:58.524803Z","iopub.status.idle":"2024-03-16T07:28:58.600564Z","shell.execute_reply.started":"2024-03-16T07:28:58.524766Z","shell.execute_reply":"2024-03-16T07:28:58.599597Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Feature engineering\n\nこのパートでは、`case_id` を使用したテーブル結合の簡単な例を見ていきます。ここでは、ポーラリスライブラリを使って読み込みと結合を行っています。ポーラリスライブラリは非常に高速で、pandasよりもメモリフットプリントがはるかに小さくなっています。\n\n---\n\n融資申請者に関する複数のテーブルを結合し、分析に必要な特徴量を抽出して新しいデータセットを作成しています。最大値や存在確認など集計処理を行い、過去の情報や融資申請者の属性、信用情報などを組み合わせています。\n\n\n**各処理のポイント**\n\n1. 最初のブロック (`train_person_1_feats_1`)\n   - `case_id` を基準に `train_person_1` テーブルをグループ化します。\n   - `mainoccupationinc_384A` カラムの最大値を求めて `mainoccupationinc_384A_max` という名前のカラムとして追加します。\n   - `incometype_1044T` カラムが \"SELFEMPLOYED\" (自営業) であるかどうかを最大値で判定し、`mainoccupationinc_384A_any_selfemployed` という名前のカラムとして追加します。\n   - 最大値を使うのは、おそらく申請者自身が過去に何度か申請していた場合でも、一度でも自営業だったかどうかを確認するためでしょう。\n\n2. 2番目のブロック (`train_person_1_feats_2`)\n   - `case_id` が 0 の行のみ抽出します。\n   - この場合、`num_group1=0` は融資を申請した本人を意味します。\n   - 残したいカラムは `housetype_905L` のみなので、他のカラムを削除します。\n   - カラム名を `person_housetype` に変更します。\n\n3. 3番目のブロック (`train_credit_bureau_b_2_feats`)\n   - `case_id` を基準に `train_credit_bureau_b_2` テーブルをグループ化します。\n   - `pmts_pmtsoverdue_635A` カラムの最大値を求めて `pmts_pmtsoverdue_635A_max` という名前のカラムとして追加します。\n   - `pmts_dpdvalue_108P` カラムが 31 を超えているかどうかを最大値で判定し、`pmts_dpdvalue_108P_over31` という名前のカラムとして追加します。\n   - 最大値を使うのは、おそらく過去の延滞履歴の中で最も悪い状況を確認するためでしょう。\n\n4. 4番目と5番目のブロック (`selected_static_cols` と `selected_static_cb_cols`)\n   - `train_static` と `train_static_cb` テーブルのカラム名をチェックしています。\n   - カラム名の最後の文字が \"A\" または \"M\" であるものだけを抽出しています。おそらく、この記号で終わるカラムが分析に重要と思われるためでしょう。\n   - 抽出したカラム名をリストにまとめています。\n\n6. 最後のブロック (`data = ...`)\n   - 以下の順番で複数のテーブルを結合します。\n     - `train_basetable`: おそらく基本情報が格納されているテーブル\n     - `train_static`: 上記で抽出したカラムを持つテーブル (結合方法: `left`、結合キー: `case_id`)\n     - `train_static_cb`: 上記で抽出したカラムを持つテーブル (結合方法: `left`、結合キー: `case_id`)\n     - `train_person_1_feats_1`: 上記で作成した集計結果テーブル (結合方法: `left`、結合キー: `case_id`)\n     - `train_person_1_feats_2`: 上記で作成した集計結果テーブル (結合方法: `left`、結合キー: `case_id`)\n     - `train_credit_bureau_b_2_feats`: 上記で作成した集計結果テーブル (結合方法: `left`、結合キー: `case_id`)\n   - `left` 結合は、結合キーが存在しない行は欠損値 (通常は `null`) で埋め込まれる結合方法です。","metadata":{}},{"cell_type":"code","source":"# We need to use aggregation functions in tables with depth > 1, so tables that contain num_group1 column or \n# also num_group2 column.\ntrain_person_1_feats_1 = train_person_1.group_by(\"case_id\").agg(\n    pl.col(\"mainoccupationinc_384A\").max().alias(\"mainoccupationinc_384A_max\"),\n    (pl.col(\"incometype_1044T\") == \"SELFEMPLOYED\").max().alias(\"mainoccupationinc_384A_any_selfemployed\")\n)\n\n# Here num_group1=0 has special meaning, it is the person who applied for the loan.\ntrain_person_1_feats_2 = train_person_1.select([\"case_id\", \"num_group1\", \"housetype_905L\"]).filter(\n    pl.col(\"num_group1\") == 0\n).drop(\"num_group1\").rename({\"housetype_905L\": \"person_housetype\"})\n\n# Here we have num_goup1 and num_group2, so we need to aggregate again.\ntrain_credit_bureau_b_2_feats = train_credit_bureau_b_2.group_by(\"case_id\").agg(\n    pl.col(\"pmts_pmtsoverdue_635A\").max().alias(\"pmts_pmtsoverdue_635A_max\"),\n    (pl.col(\"pmts_dpdvalue_108P\") > 31).max().alias(\"pmts_dpdvalue_108P_over31\")\n)\n\n# We will process in this examples only A-type and M-type columns, so we need to select them.\nselected_static_cols = []\nfor col in train_static.columns:\n    if col[-1] in (\"A\", \"M\"):\n        selected_static_cols.append(col)\nprint(selected_static_cols)\n\nselected_static_cb_cols = []\nfor col in train_static_cb.columns:\n    if col[-1] in (\"A\", \"M\"):\n        selected_static_cb_cols.append(col)\nprint(selected_static_cb_cols)\n\n# Join all tables together.\ndata = train_basetable.join(\n    train_static.select([\"case_id\"]+selected_static_cols), how=\"left\", on=\"case_id\"\n).join(\n    train_static_cb.select([\"case_id\"]+selected_static_cb_cols), how=\"left\", on=\"case_id\"\n).join(\n    train_person_1_feats_1, how=\"left\", on=\"case_id\"\n).join(\n    train_person_1_feats_2, how=\"left\", on=\"case_id\"\n).join(\n    train_credit_bureau_b_2_feats, how=\"left\", on=\"case_id\"\n)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:58.601802Z","iopub.execute_input":"2024-03-16T07:28:58.602111Z","iopub.status.idle":"2024-03-16T07:28:59.964101Z","shell.execute_reply.started":"2024-03-16T07:28:58.602086Z","shell.execute_reply":"2024-03-16T07:28:59.963094Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"テストデータも同様に処理します。","metadata":{}},{"cell_type":"code","source":"test_person_1_feats_1 = test_person_1.group_by(\"case_id\").agg(\n    pl.col(\"mainoccupationinc_384A\").max().alias(\"mainoccupationinc_384A_max\"),\n    (pl.col(\"incometype_1044T\") == \"SELFEMPLOYED\").max().alias(\"mainoccupationinc_384A_any_selfemployed\")\n)\n\ntest_person_1_feats_2 = test_person_1.select([\"case_id\", \"num_group1\", \"housetype_905L\"]).filter(\n    pl.col(\"num_group1\") == 0\n).drop(\"num_group1\").rename({\"housetype_905L\": \"person_housetype\"})\n\ntest_credit_bureau_b_2_feats = test_credit_bureau_b_2.group_by(\"case_id\").agg(\n    pl.col(\"pmts_pmtsoverdue_635A\").max().alias(\"pmts_pmtsoverdue_635A_max\"),\n    (pl.col(\"pmts_dpdvalue_108P\") > 31).max().alias(\"pmts_dpdvalue_108P_over31\")\n)\n\ndata_submission = test_basetable.join(\n    test_static.select([\"case_id\"]+selected_static_cols), how=\"left\", on=\"case_id\"\n).join(\n    test_static_cb.select([\"case_id\"]+selected_static_cb_cols), how=\"left\", on=\"case_id\"\n).join(\n    test_person_1_feats_1, how=\"left\", on=\"case_id\"\n).join(\n    test_person_1_feats_2, how=\"left\", on=\"case_id\"\n).join(\n    test_credit_bureau_b_2_feats, how=\"left\", on=\"case_id\"\n)","metadata":{"execution":{"iopub.status.busy":"2024-03-28T12:56:53.464291Z","iopub.execute_input":"2024-03-28T12:56:53.464690Z","iopub.status.idle":"2024-03-28T12:56:53.478944Z","shell.execute_reply.started":"2024-03-28T12:56:53.464638Z","shell.execute_reply":"2024-03-28T12:56:53.478132Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"機械学習のモデル構築のために、元のデータセットを `case_id` を基準に分割し、Pandas データフレームに変換しています。訓練データ、検証データ、本番データに分割するだけでなく、特徴量と正解ラベルを分離する処理も行っています。\n\n**各処理のポイント**\n\n1. 最初のブロック (`case_ids = ...`)\n   - `data` フレームから `case_id` カラムを取り出し、重複を除きます (`unique()`).\n   - 取得した `case_id` をシャッフルします (`shuffle()`). シャッフルにはシード値 (`seed=1`) が設定されており、毎回同じ順番にならないようにしています。\n   - シャッフルした `case_id` を訓練データ (`case_ids_train`) とテストデータ (`case_ids_test`) に分割します (`train_test_split()`). 分割比率は、訓練データ:テストデータ = 6:4 です。\n   - さらにテストデータを検証データ (`case_ids_valid`) と本番データ (`case_ids_test`) に分割します。 分割比率は、検証データ:本番データ = 1:1 です。\n\n2. 2番目のブロック (`cols_pred = ...`)\n   - `data` フレームのカラム名をチェックしています。\n   - カラム名の最後がアルファベットの大文字 (isupper()) で、それ以外は小文字 (islower()) である場合、そのカラム名を抽出しています。\n   - この条件に当てはまるカラム名が、おそらく予測対象のカラム (target) を説明するのに重要と思われる特徴量 (feature) でしょう。\n\n3. 3番目のブロック (`def from_polars_to_pandas ...`)\n   - `from_polars_to_pandas` という関数を定義しています。\n   - この関数は、`case_ids` (おそらく `case_id` のリスト) を受け取り、分割されたデータセットを返します。\n   - 引数の `case_ids` に含まれる `case_id` を持つ行のみを `data` フレームから抽出し、Pandas データフレームに変換します。\n   - 返却値はタプルになっており、3つの要素を持ちます。\n     - 1つ目は、`case_id`, `WEEK_NUM`, `target` カラムを含むベース情報\n     - 2つ目は、予測対象以外の全てのカラムを含む特徴量\n     - 3つ目は、`target` カラムのみを含む正解ラベル\n\n4. 4番目のブロックから6番目のブロック (`base_train, X_train, y_train = ...`)\n   - `from_polars_to_pandas` 関数を使って、訓練データ、検証データ、本番データを分割し、Pandas データフレームに変換します。\n   - 訓練データは、ベース情報 (`base_train`)、特徴量 (`X_train`)、正解ラベル (`y_train`) に分割されます。\n   - 検証データと本番データも同様に分割されます。\n\n5. 最後のブロック (`for df in ...`)\n   - forループを使って、訓練データの特徴量 (`X_train`)、検証データの特徴量 (`X_valid`)、本番データの特徴量 (`X_test`) をイテレートしています。\n   - 各特徴量に対して、`convert_strings` という関数が呼び出されていますが、この関数の内容はこのコードからはわかりません。おそらく文字列型のデータを、モデルが扱いやすいように変換する処理だと思われます。","metadata":{}},{"cell_type":"code","source":"case_ids = data[\"case_id\"].unique().shuffle(seed=1)\ncase_ids_train, case_ids_test = train_test_split(case_ids, train_size=0.6, random_state=1)\ncase_ids_valid, case_ids_test = train_test_split(case_ids_test, train_size=0.5, random_state=1)\n\ncols_pred = []\nfor col in data.columns:\n    if col[-1].isupper() and col[:-1].islower():\n        cols_pred.append(col)\n\nprint(cols_pred)\n\ndef from_polars_to_pandas(case_ids: pl.DataFrame) -> pl.DataFrame:\n    return (\n        data.filter(pl.col(\"case_id\").is_in(case_ids))[[\"case_id\", \"WEEK_NUM\", \"target\"]].to_pandas(),\n        data.filter(pl.col(\"case_id\").is_in(case_ids))[cols_pred].to_pandas(),\n        data.filter(pl.col(\"case_id\").is_in(case_ids))[\"target\"].to_pandas()\n    )\n\nbase_train, X_train, y_train = from_polars_to_pandas(case_ids_train)\nbase_valid, X_valid, y_valid = from_polars_to_pandas(case_ids_valid)\nbase_test, X_test, y_test = from_polars_to_pandas(case_ids_test)\n\nfor df in [X_train, X_valid, X_test]:\n    df = convert_strings(df)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:28:59.983480Z","iopub.execute_input":"2024-03-16T07:28:59.983809Z","iopub.status.idle":"2024-03-16T07:29:07.402772Z","shell.execute_reply.started":"2024-03-16T07:28:59.983779Z","shell.execute_reply":"2024-03-16T07:29:07.401691Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f\"Train: {X_train.shape}\")\nprint(f\"Valid: {X_valid.shape}\")\nprint(f\"Test: {X_test.shape}\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T07:29:07.404098Z","iopub.execute_input":"2024-03-16T07:29:07.404382Z","iopub.status.idle":"2024-03-16T07:29:07.409575Z","shell.execute_reply.started":"2024-03-16T07:29:07.404358Z","shell.execute_reply":"2024-03-16T07:29:07.408490Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Training LightGBM\n\n以下は、LightGBM を用いた最小限のトレーニング例です。\n\n---\n\n## コード解説\n\nLightGBM ライブラリを使って、分類タスク向けの勾配ブースティング決定木モデルを構築しています。学習パラメータを設定し、検証データによる評価と早期停止を組み合わせてモデルを訓練しています。\n\n**各処理のポイント**\n\n1. 最初のブロック (`lgb_train = ...`, `lgb_valid = ...`)\n   - LightGBM用のデータセットを作成しています。\n     - `lgb_train` は訓練データセットで、特徴量 (`X_train`) と正解ラベル (`y_train`) を `lgb.Dataset` クラスに格納しています。\n     - `lgb_valid` は検証データセットで、特徴量 (`X_valid`) と正解ラベル (`y_valid`) を格納しています。\n     - `reference=lgb_train` を指定しているのは、検証データセットの評価指標を計算する際に、訓練データセットの統計情報を利用できるようにするためです。\n\n2. 2番目のブロック (`params = ...`)\n   - LightGBM の学習パラメータを `params` という辞書に格納しています。\n     - \"boosting_type\": \"gbdt\" は、勾配ブースティング決定木を使用することを指定しています。\n     - \"objective\": \"binary\" は、分類タスク (binary classification) であることを指定しています。\n     - \"metric\": \"auc\" は、評価指標として AUC (Area Under the ROC Curve) を使用することを指定しています。\n     - \"max_depth\": 3 は、決定木の最大深さを 3 に設定しています。\n     - \"num_leaves\": 31 は、決定木の最大葉数を 31 に設定しています。\n     - \"learning_rate\": 0.05 は、学習率を 0.05 に設定しています。\n     - \"feature_fraction\": 0.9 は、各イテレーションで使用する特徴量比率を 0.9 に設定しています。\n     - \"bagging_fraction\": 0.8 は、バギング (bagging) で使用するデータ比率を 0.8 に設定しています。\n     - \"bagging_freq\": 5 は、バギングを行う頻度を 5 イテレーションごとにすることを指定しています。\n     - \"n_estimators\": 1000 は、決定木を最大で 1000 個生成することを指定しています。\n     - \"verbose\": -1 は、学習過程の出力を表示しないことを指定しています。\n\n3. 3番目のブロック (`gbm = lgb.train(...)`)\n   - `lgb.train` 関数を使って、LightGBM モデルを構築しています。\n     - 第 1 引数 (`params`) には、学習パラメータを渡します。\n     - 第 2 引数 (`lgb_train`) には、訓練データセットを渡します。\n     - `valid_sets` 引数には、検証データセット (`lgb_valid`) を渡します。検証データセットはモデルの評価に使用されます。\n     - `callbacks` 引数には、コールバック関数をリストで渡します。\n       - `lgb.log_evaluation(50)` は、50 イテレーションごとに評価結果をログ出力するように設定します。\n       - `lgb.early_stopping(10)` は、10 回連続して検証データセットの評価指標が改善しなくなったら学習を打ち切るように設定します。","metadata":{}},{"cell_type":"code","source":"lgb_train = lgb.Dataset(X_train, label=y_train)\nlgb_valid = lgb.Dataset(X_valid, label=y_valid, reference=lgb_train)\n\nparams = {\n    \"boosting_type\": \"gbdt\",\n    \"objective\": \"binary\",\n    \"metric\": \"auc\",\n    \"max_depth\": 3,\n    \"num_leaves\": 31,\n    \"learning_rate\": 0.05,\n    \"feature_fraction\": 0.9,\n    \"bagging_fraction\": 0.8,\n    \"bagging_freq\": 5,\n    \"n_estimators\": 1000,\n    \"verbose\": -1,\n}\n\ngbm = lgb.train(\n    params,\n    lgb_train,\n    valid_sets=lgb_valid,\n    callbacks=[lgb.log_evaluation(50), lgb.early_stopping(10)]\n)","metadata":{"execution":{"iopub.status.busy":"2024-03-28T13:01:20.291623Z","iopub.execute_input":"2024-03-28T13:01:20.291980Z","iopub.status.idle":"2024-03-28T13:02:55.823408Z","shell.execute_reply.started":"2024-03-28T13:01:20.291954Z","shell.execute_reply":"2024-03-28T13:02:55.822536Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"AUCによる評価結果と安定性指標との比較を以下に示しています。","metadata":{}},{"cell_type":"code","source":"for base, X in [(base_train, X_train), (base_valid, X_valid), (base_test, X_test)]:\n    y_pred = gbm.predict(X, num_iteration=gbm.best_iteration)\n    base[\"score\"] = y_pred\n\nprint(f'The AUC score on the train set is: {roc_auc_score(base_train[\"target\"], base_train[\"score\"])}') \nprint(f'The AUC score on the valid set is: {roc_auc_score(base_valid[\"target\"], base_valid[\"score\"])}') \nprint(f'The AUC score on the test set is: {roc_auc_score(base_test[\"target\"], base_test[\"score\"])}')  ","metadata":{"execution":{"iopub.status.busy":"2024-03-28T13:04:36.854535Z","iopub.execute_input":"2024-03-28T13:04:36.855726Z","iopub.status.idle":"2024-03-28T13:05:06.791621Z","shell.execute_reply.started":"2024-03-28T13:04:36.855672Z","shell.execute_reply":"2024-03-28T13:05:06.790557Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"機械学習モデルの**安定性**を評価する関数 `gini_stability` を定義し、訓練データ、検証データ、本番データに対して安定性スコアを計算している部分です。\n\n**gini_stability 関数の詳細**\n\n1. 引数\n   - `base` : モデルの予測結果 (スコア) が格納されたベースデータフレーム\n\n2. 内部処理\n   - まず、`base` データフレームから、\"WEEK_NUM\", \"target\", \"score\" の 3 カラムのみ抽出します。\n   - 次に、\"WEEK_NUM\" でソートし、同じ週ごとのグループに対して集計処理を行います (`groupby`).\n   - 集計処理の中身は、以下のようになっています。\n     - `lambda x: ...` は、匿名関数を使ってグループごとの処理を定義しています。\n     - `roc_auc_score(x[\"target\"], x[\"score\"])` で、ROC AUC スコアを計算します。ROC AUC スコアは、レシーバーオペレーティング特性 (ROC) 曲線の下の面積であり、モデルの識別能力を評価する指標です。\n     - 2 倍にして、1 から引くことで、ジニ係数に変換しています。ジニ係数もまた、分類モデルの性能を評価する指標で、値が 1 に近いほど性能が良いことを示します。\n   - この処理により、各週ごとのジニ係数がリスト化されます ( `gini_in_time` )\n\n3. 安定性スコアの計算\n   - `x` には、週ごとのインデックス (0 から始まる連番) を代入します。\n   - `y` には、計算されたジニ係数リスト ( `gini_in_time` ) を代入します。\n   - `np.polyfit(x, y, 1)` を使って、`x`と `y` の一次関数フィッティングを行います。\n     - このフィッティングにより、直線の傾き (`a`) と切片 (`b`) が求められます。\n   - `y_hat` には、フィッティングされた直線による予測値を代入します。\n   - `residuals` には、実際のジニ係数と予測値との差分 (残差) を代入します。\n   - `res_std` には、残差の標準偏差を代入します。\n   - `avg_gini` には、ジニ係数の平均値を代入します。\n   - 最後に、以下の 3 項の和を返します。\n     - 最初の項 (`avg_gini`) は、平均的なジニ係数\n     - 2 番目の項 (`w_fallingrate * min(0, a)`) は、傾きが負 (右下がりの直線) になるほど大きくなる項です。おそらく、ジニ係数が低下する傾向 (成績が下がっていく傾向) をペナルティとして加えています。`w_fallingrate` は重みパラメータです。\n     - 3 番目の項 (`w_resstd * res_std`) は、残差の標準偏差を掛けた項です。残差の標準偏差が大きいほど、ジニ係数のばらつきが大きい (安定性が悪い) ことになるので、こちらもペナルティとして加えています。`w_resstd` は重みパラメータです。","metadata":{}},{"cell_type":"code","source":"def gini_stability(base, w_fallingrate=88.0, w_resstd=-0.5):\n    gini_in_time = base.loc[:, [\"WEEK_NUM\", \"target\", \"score\"]]\\\n        .sort_values(\"WEEK_NUM\")\\\n        .groupby(\"WEEK_NUM\")[[\"target\", \"score\"]]\\\n        .apply(lambda x: 2*roc_auc_score(x[\"target\"], x[\"score\"])-1).tolist()\n    \n    x = np.arange(len(gini_in_time))\n    y = gini_in_time\n    a, b = np.polyfit(x, y, 1)\n    y_hat = a*x + b\n    residuals = y - y_hat\n    res_std = np.std(residuals)\n    avg_gini = np.mean(gini_in_time)\n    return avg_gini + w_fallingrate * min(0, a) + w_resstd * res_std\n\nstability_score_train = gini_stability(base_train)\nstability_score_valid = gini_stability(base_valid)\nstability_score_test = gini_stability(base_test)\n\nprint(f'The stability score on the train set is: {stability_score_train}') \nprint(f'The stability score on the valid set is: {stability_score_valid}') \nprint(f'The stability score on the test set is: {stability_score_test}')  ","metadata":{"execution":{"iopub.status.busy":"2024-03-28T13:05:06.793736Z","iopub.execute_input":"2024-03-28T13:05:06.794179Z","iopub.status.idle":"2024-03-28T13:05:07.869492Z","shell.execute_reply.started":"2024-03-28T13:05:06.794138Z","shell.execute_reply":"2024-03-28T13:05:07.868415Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Submission\n\n以下は、提出データセットに対するスコアリング処理です。新しいカテゴリへの対応と、スコアの保存について説明します。\n\n---\n\n","metadata":{}},{"cell_type":"markdown","source":"## コード解説\n\nこのコードは、本番データの前処理として、訓練データに存在しないカテゴリを \"Unknown\" に置き換える処理と、カテゴリ型のデータをモデルが扱いやすい形式に変換する処理を行っています。前処理を行った後、LightGBM モデルを使って本番データの予測を行い、`y_submission_pred` に結果を格納しています。\n\n**各処理のポイント**\n\n1. 最初のブロック (`X_submission = ...`)\n   - `data_submission` フレームから、予測対象のカラム (`cols_pred`) を抽出し、Pandas データフレームに変換します (`to_pandas()`).\n\n2. 2 番目のブロック (`X_submission = ...`)\n   - `convert_strings` という関数が呼び出されていますが、この関数の内容は不明です。おそらく文字列型のデータを、モデルが扱いやすいように変換する処理だと思われます。\n\n3. 3 番目のブロック (`categorical_cols = ...`)\n   - 訓練データ (`X_train`) から、カテゴリ型 (`category`) のカラム名を取得します (`select_dtypes(include=['category'])`).\n\n4. ループブロック (`for col in categorical_cols: ...`)\n   - カテゴリ型の各カラムについて、以下の処理を行います。\n     - `train_categories`: 訓練データのカラムのカテゴリ集合を取得します (`cat.categories`).\n     - `submission_categories`: 提出データのカラムのカテゴリ集合を取得します (`cat.categories`).\n     - `new_categories`: 提出データに存在して、訓練データに存在しないカテゴリの集合を求めます (`-`).\n     - もし `new_categories` が空でなければ、提出データのカラムで、新しいカテゴリに属する行の値を \"Unknown\" に置き換えます (`isin`, `loc`).\n     - `new_dtype`: 訓練データのカテゴリを保持した CategoricalDtype を作成します。\n       - `categories`: カテゴリのリスト (`train_categories`) を指定します。\n       - `ordered=True`: カテゴリ間に順序関係があることを指定します (おそらく重要度など順序があることがわかっているためでしょう)。\n     - 訓練データと提出データのカラムを、作成した CategoricalDtype に変換します (`astype`).\n\n5. 最後のブロック (`y_submission_pred = ...`)\n   - LightGBM モデル (`gbm`) を使って、提出データ (`X_submission`) を予測します。\n     - `num_iteration=gbm.best_iteration` を指定しているのは、学習時に得られた最良のイテレーションの結果を使って予測を行うためです。","metadata":{}},{"cell_type":"code","source":"X_submission = data_submission[cols_pred].to_pandas()\nX_submission = convert_strings(X_submission)\ncategorical_cols = X_train.select_dtypes(include=['category']).columns\n\nfor col in categorical_cols:\n    train_categories = set(X_train[col].cat.categories)\n    submission_categories = set(X_submission[col].cat.categories)\n    new_categories = submission_categories - train_categories\n    X_submission.loc[X_submission[col].isin(new_categories), col] = \"Unknown\"\n    new_dtype = pd.CategoricalDtype(categories=train_categories, ordered=True)\n    X_train[col] = X_train[col].astype(new_dtype)\n    X_submission[col] = X_submission[col].astype(new_dtype)\n\ny_submission_pred = gbm.predict(X_submission, num_iteration=gbm.best_iteration)","metadata":{"execution":{"iopub.status.busy":"2024-03-28T13:08:53.554322Z","iopub.execute_input":"2024-03-28T13:08:53.554708Z","iopub.status.idle":"2024-03-28T13:08:53.637225Z","shell.execute_reply.started":"2024-03-28T13:08:53.554679Z","shell.execute_reply":"2024-03-28T13:08:53.636319Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission = pd.DataFrame({\n    \"case_id\": data_submission[\"case_id\"].to_numpy(),\n    \"score\": y_submission_pred\n}).set_index('case_id')\nsubmission.to_csv(\"./submission.csv\")","metadata":{"execution":{"iopub.status.busy":"2024-03-28T13:08:55.253840Z","iopub.execute_input":"2024-03-28T13:08:55.254454Z","iopub.status.idle":"2024-03-28T13:08:55.261985Z","shell.execute_reply.started":"2024-03-28T13:08:55.254420Z","shell.execute_reply":"2024-03-28T13:08:55.261073Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}