{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":104854,"databundleVersionId":12637092,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Контакты:\n\n* Telegram: @joulin\n* Почта: matthewthedigital@gmail.com","metadata":{}},{"cell_type":"code","source":"%%capture\nrequirements = \"\"\"\nfireducks>=1.3.1\n\"\"\"\n\n!echo \"{requirements}\" > requirements.txt\n%pip install -r requirements.txt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:28:52.763375Z","iopub.execute_input":"2025-07-14T09:28:52.763667Z","iopub.status.idle":"2025-07-14T09:29:01.097522Z","shell.execute_reply.started":"2025-07-14T09:28:52.763646Z","shell.execute_reply":"2025-07-14T09:29:01.096435Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from typing import Optional\nfrom datetime import date, datetime\nfrom dateutil.relativedelta import relativedelta\n\nimport numpy as np\n# fast data processing with Pandas API support\nimport fireducks.pandas as pd\nimport matplotlib.pyplot as plt\n\n# models and training\nfrom sklearn.model_selection import train_test_split\nimport xgboost as xgb\nimport optuna","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:01.098934Z","iopub.execute_input":"2025-07-14T09:29:01.099210Z","iopub.status.idle":"2025-07-14T09:29:02.604064Z","shell.execute_reply.started":"2025-07-14T09:29:01.099175Z","shell.execute_reply":"2025-07-14T09:29:02.603500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Загрузка данных\n\ndf_train = pd.read_parquet(\"/kaggle/input/alpha-summer-challenge/train.pa\")\ndf_txn   = pd.read_parquet(\"/kaggle/input/alpha-summer-challenge/df_transaction.pa\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:02.604760Z","iopub.execute_input":"2025-07-14T09:29:02.605030Z","iopub.status.idle":"2025-07-14T09:29:02.622186Z","shell.execute_reply.started":"2025-07-14T09:29:02.605014Z","shell.execute_reply":"2025-07-14T09:29:02.621669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Определяем клиентов в тесте\n\ntest_clients = (\n    df_txn.loc[~df_txn.client_num.isin(df_train.client_num), \"client_num\"]\n    .unique()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:02.623606Z","iopub.execute_input":"2025-07-14T09:29:02.623838Z","iopub.status.idle":"2025-07-14T09:29:08.835902Z","shell.execute_reply.started":"2025-07-14T09:29:02.623822Z","shell.execute_reply":"2025-07-14T09:29:08.835329Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Посмотрим на объемы обучающей и тестовой выборки\n# Выведим количество клиентов и количество транзакций\n\ndef show_info(name: str, cl_num: int, txn_num: int):\n    print(f\"{name}:\\t{cl_num:,} clients\\t{txn_num:,} txn\")\n\n\ntrain_txn_num = df_txn.client_num.isin(df_train.client_num).sum()\n\nshow_info(\"Train\", len(df_train), train_txn_num)\nshow_info(\"Test\", len(test_clients), train_txn_num)\nshow_info(\"Total\", len(df_train) + len(test_clients), len(df_txn))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:08.836909Z","iopub.execute_input":"2025-07-14T09:29:08.837147Z","iopub.status.idle":"2025-07-14T09:29:08.931782Z","shell.execute_reply.started":"2025-07-14T09:29:08.837120Z","shell.execute_reply":"2025-07-14T09:29:08.931027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Смотрим распределение таргетов\n(\n    df_train[\"target\"]\n    .value_counts(normalize=True)\n    .sort_index()\n    .reset_index()\n    .plot.bar(x=\"target\", y=\"proportion\", rot=0)\n)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:08.932660Z","iopub.execute_input":"2025-07-14T09:29:08.932908Z","iopub.status.idle":"2025-07-14T09:29:09.187553Z","shell.execute_reply.started":"2025-07-14T09:29:08.932878Z","shell.execute_reply":"2025-07-14T09:29:09.186822Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Основные фичи:\n1. Cnt/sum/min/max/avg/median сумм транзакций за последние 14, 30, 92 дней\n2. Sum/cnt тразнакций в разрезе по всем mcc кодам с окнами 14, 30 и 92 дней\n3. Средне-дневное количество и сумма транзакций за 14, 30 и 92 дней\n4. Количество дней с транзакциями за 14, 30 и 92 дней\n5. Min/max/avg/median количество и сумма транзакций по дням с окнами 14, 30 и 92 дней\n6. Max/avg количество дней без транзакций за 14, 30 и 92 дней\n7. Дней до последней транзакции\n8. Diff sum транзакций:\n* текущие 14 дней - предыдущие 14 дней,\n* текущий месяц - прошлый месяц","metadata":{"execution":{"iopub.status.busy":"2025-07-09T11:51:23.996894Z","iopub.execute_input":"2025-07-09T11:51:23.997108Z","iopub.status.idle":"2025-07-09T11:51:24.000745Z","shell.execute_reply.started":"2025-07-09T11:51:23.997088Z","shell.execute_reply":"2025-07-09T11:51:24.000088Z"}}},{"cell_type":"code","source":"# Посмотрим временной период\nmax_date_time, min_date_time = df_txn['date_time'].max(), df_txn['date_time'].min()\n\nprint(\"Максимальная дата:\", max_date_time)\nprint(\"Минимальная дата:\", min_date_time)\nprint(\"Длина периода:\", max_date_time - min_date_time)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:17.333558Z","iopub.execute_input":"2025-07-14T09:29:17.333820Z","iopub.status.idle":"2025-07-14T09:29:17.351983Z","shell.execute_reply.started":"2025-07-14T09:29:17.333802Z","shell.execute_reply":"2025-07-14T09:29:17.351346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"max_date_time_txn = df_txn.loc[df_txn['date_time'] == max_date_time, \"date_time\"]\nprint(\n    \"Количество транзакций в максимальный момент времени:\",\n    len(max_date_time_txn)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:19.331440Z","iopub.execute_input":"2025-07-14T09:29:19.331712Z","iopub.status.idle":"2025-07-14T09:29:19.355819Z","shell.execute_reply.started":"2025-07-14T09:29:19.331691Z","shell.execute_reply":"2025-07-14T09:29:19.355062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Для дальнейшего удобства вычтем у них 1 секунду, \n# чтобы масимальная дата (2024-10-01) была не включительна\ndf_txn.loc[max_date_time_txn.index, \"date_time\"] -= pd.Timedelta(seconds=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:22.174437Z","iopub.execute_input":"2025-07-14T09:29:22.174712Z","iopub.status.idle":"2025-07-14T09:29:24.827269Z","shell.execute_reply.started":"2025-07-14T09:29:22.174691Z","shell.execute_reply":"2025-07-14T09:29:24.826701Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_window_features(\n    wind_df: pd.DataFrame,\n    wind_postfix: Optional[str] = None,\n    with_mcc_codes: bool = True\n) -> pd.DataFrame:\n    \"\"\"\n        Функция для получения фичей (см. описание фичей выше) за определённое окно\n\n        Params:\n            wind_df: pd.DataFrame\n                Исходные данные за определённое окно\n            wind_postfix: str or None, deafult None\n                Постфикс, который будет добавлен названиям всех фичей\n            with_mcc_codes: bool, deafult True\n                Необходимо ли рассчитывать группу фичей в разрезе всех MCC-кодов\n\n        Returns:\n            DataFrame с фичами за окно\n    \"\"\"\n    start_period_dt = wind_df[\"date_time\"].min()\n    num_days = (wind_df[\"date_time\"].max() - start_period_dt).days + 1\n    print(f\"Calc features for window {num_days} days - START\")\n    \n    # General features (cnt, sum, min, max, avg)\n    general_df = wind_df.groupby(\"client_num\").agg(\n        txn_cnt=(\"amount\", \"count\"),\n        txn_sum=(\"amount\", \"sum\"),\n        txn_avg=(\"amount\", \"mean\"),\n        txn_median=('amount', 'median'),\n        txn_min_sum=(\"amount\", \"min\"),\n        txn_max_sum=(\"amount\", \"max\")\n    )\n    general_df[\"wind_txn_cnt_avg\"] = general_df[\"txn_cnt\"] / num_days\n    general_df[\"wind_txn_sum_avg\"] = general_df[\"txn_sum\"] / num_days\n\n    # Features by dates\n    days_df = wind_df.groupby([\"client_num\", \"txn_date\"]).agg(\n        txn_cnt=(\"amount\", 'count'),\n        txn_sum=(\"amount\", 'sum')\n    ).reset_index()\n\n    days_df[\"prev_txn_date\"] = (\n        days_df\n        .sort_values(by=['txn_date'], ascending=True)\n        .groupby(\"client_num\")['txn_date'].shift(1)\n    )\n    days_df[\"prev_txn_date\"] = days_df[\"prev_txn_date\"].fillna(\n        start_period_dt.date() - relativedelta(days=1)\n    )\n    days_df[\"txn_gap_days\"] = (days_df['txn_date'] - days_df['prev_txn_date']).dt.days\n\n    days_df = days_df.groupby([\"client_num\"]).agg(\n        txn_days_cnt=(\"txn_date\", \"count\"),\n        day_txn_cnt_min=(\"txn_cnt\", \"min\"),\n        day_txn_cnt_max=(\"txn_cnt\", \"max\"),\n        day_txn_cnt_avg=(\"txn_cnt\", \"mean\"),\n        day_txn_cnt_median=('txn_cnt', 'median'),\n        day_txn_sum_min=(\"txn_sum\", \"min\"),\n        day_txn_sum_max=(\"txn_sum\", \"max\"),\n        day_txn_sum_avg=(\"txn_sum\", \"mean\"),\n        day_txn_sum_median=('txn_sum', 'median'),\n        txn_max_days_gap=(\"txn_gap_days\", \"max\"),\n        txn_avg_days_gap=(\"txn_gap_days\", \"mean\")\n    )\n\n    if with_mcc_codes:\n        # MCC features: cnt/sum by each mcc code\n        mcc_df = wind_df.groupby([\"client_num\", \"mcc_code\"]).agg(\n            txn_cnt=(\"amount\", \"count\"),\n            txn_sum=(\"amount\", \"sum\")\n        ).reset_index()\n    \n        mcc_df = mcc_df.pivot(\n            index='client_num', columns='mcc_code', \n            values=[\"txn_cnt\", \"txn_sum\"]\n        )\n        mcc_df.columns = [\"mcc_\" + \"_\".join(col[::-1]) for col in mcc_df.columns]\n\n    # Join to result DataFrame\n    res_df = general_df.merge(days_df, left_index=True, right_index=True, how='inner')\n    if with_mcc_codes:\n        res_df = res_df.merge(mcc_df, left_index=True, right_index=True, how='inner')\n    res_df = res_df.fillna(0)\n\n    if wind_postfix:\n        res_df.columns = [\"_\".join([col, wind_postfix]) for col in res_df.columns]\n\n    print(f\"Calc features for window {num_days} days - DONE\")\n\n    return res_df\n\n\ndef get_diff_features(\n    period_features: pd.DataFrame,\n    next_period_df: pd.DataFrame, \n    wind_postfix: Optional[str] = None\n) -> pd.DataFrame:\n    \"\"\"\n        Функция для получения diff фичей между текущим окном и предыдущим\n\n        Params:\n            period_features:\n                Рассчитанные фичи для текущего периода (см. get_window_features)\n            next_period_df:\n                Исходные данные для следующего периода\n            wind_postfix: str or None, deafult None\n                    Постфикс, который будет добавлен названиям всех фичей\n        Returns:\n            DataFrame с diff фичaми\n    \"\"\"\n    print(f\"Calc diff features for window {wind_postfix} - START\")\n\n    next_period_features = (\n        next_period_df\n        .groupby([\"client_num\", \"txn_date\"])\n        .agg(\n            txn_cnt=(\"amount\", 'count'),\n            txn_sum=(\"amount\", 'sum')\n        )\n        .reset_index()\n        .groupby(\"client_num\")\n        .agg(\n            next_txn_days_cnt=(\"txn_date\", \"count\"),\n            next_txn_cnt=(\"txn_cnt\", 'sum'),\n            next_txn_sum=(\"txn_sum\", 'sum')\n        )\n    )\n\n    features_list = [\"txn_days_cnt\", \"txn_cnt\", \"txn_sum\"]\n    period_cols = [\"_\".join([feat_name, wind_postfix]) for feat_name in features_list]\n    \n    periods_df = next_period_features.merge(\n        period_features[period_cols], \n        left_index=True, right_index=True,\n        how='outer'\n    )\n    periods_df = periods_df.fillna(0)\n    \n    for period_col, feature_name in zip(period_cols, features_list):\n        periods_df[f\"diff_{feature_name}\"] = periods_df[period_col] - periods_df[f\"next_{feature_name}\"]    \n        periods_df.drop(columns=[period_col, f\"next_{feature_name}\"], inplace=True)\n    \n    if wind_postfix:\n        periods_df.columns = [\"_\".join([col, wind_postfix]) for col in periods_df.columns]\n\n    print(f\"Calc diff features for window {wind_postfix} - DONE\")\n    \n    return periods_df    \n\ndef get_transactions_features(df_txn: pd.DataFrame, score_date: date) -> pd.DataFrame:\n    \"\"\"\n        Функция для рассчёта и получения полного DataFrame с фичами \n        на основе исходных данных о транзакциях\n        Все null значения заполняются нулями\n    \n        Params:\n            df_txn: pd.DataFrame\n                DataFrame с исходными данными по транзакциям\n            score_date: date\n                Дата расчёта, первый день месяца\n\n        Returns:\n            DataFrame с фичами для обучения модели\n    \"\"\"\n    df_txn[\"txn_date\"] = df_txn['date_time'].dt.date\n\n    windows = [\n        # Structure: (<num units>, <units>, <with_mcc_codes flag (optional)>)\n        # (7, \"days\", False),\n        (14, \"days\"),\n        (1, \"months\"),\n        # (45, \"days\"),\n        (3, \"months\")\n    ]\n\n    wind_features = []\n    for window in windows:\n        wind_postfix = f\"{window[0]}{window[1][0]}\"\n        if window != (3, \"months\"):\n            start_wind_date = score_date - relativedelta(**{window[1]: window[0]})\n            wind_df = df_txn[df_txn[\"txn_date\"] >= start_wind_date]\n        else:\n            start_wind_date = None\n            wind_df = df_txn\n\n        with_mcc_codes = len(window) <= 2 or window[2]\n\n        wind_features_df = get_window_features(\n            wind_df, wind_postfix=wind_postfix, with_mcc_codes=with_mcc_codes\n        )\n        wind_features.append(wind_features_df)\n\n        if start_wind_date:\n            next_wind_start_date = start_wind_date - relativedelta(**{window[1]: window[0]})\n            next_wind_df = df_txn[\n                (df_txn[\"txn_date\"] >= next_wind_start_date) & \n                (df_txn[\"txn_date\"] < start_wind_date)\n            ]\n            wind_diff_features_df = get_diff_features(\n                wind_features_df, next_wind_df, wind_postfix=wind_postfix\n            )\n            wind_features.append(wind_diff_features_df)\n\n    # Other features\n    last_txn = df_txn.groupby(\"client_num\").agg(last_txn_date=(\"date_time\", 'max'))\n    last_txn[\"last_txn_days\"] = (score_date - last_txn['last_txn_date']).dt.days\n    last_txn.drop(columns=[\"last_txn_date\"], inplace=True)\n\n    # Join result DataFrame\n    print(\"Join features - START\")\n\n    res_df = last_txn\n    for wind_features_df in wind_features:\n        res_df = res_df.merge(wind_features_df, left_index=True, right_index=True, how='left')\n\n    res_df = res_df.fillna(0).reset_index()\n\n    print(\"Join features - DONE\")\n    \n    return res_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:26.463548Z","iopub.execute_input":"2025-07-14T09:29:26.463811Z","iopub.status.idle":"2025-07-14T09:29:26.481498Z","shell.execute_reply.started":"2025-07-14T09:29:26.463792Z","shell.execute_reply":"2025-07-14T09:29:26.480707Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\n# Считаем фичи\nscore_date = max_date_time.date()\nprint(\"Score date:\", score_date)\n\nfeatures_df = get_transactions_features(df_txn, score_date=score_date)\nprint(\"Features shape:\", features_df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:29:34.405884Z","iopub.execute_input":"2025-07-14T09:29:34.406464Z","iopub.status.idle":"2025-07-14T09:30:11.869404Z","shell.execute_reply.started":"2025-07-14T09:29:34.406441Z","shell.execute_reply":"2025-07-14T09:30:11.868713Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"features_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:11.871006Z","iopub.execute_input":"2025-07-14T09:30:11.871213Z","iopub.status.idle":"2025-07-14T09:30:11.912859Z","shell.execute_reply.started":"2025-07-14T09:30:11.871195Z","shell.execute_reply":"2025-07-14T09:30:11.912254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Посмотрим распрделение некоторых из полученных фичей\n\nplot_features = [\n    'last_txn_days', ('txn_cnt_3m', False),\n    'txn_days_cnt_14d', 'txn_days_cnt_1m',\n    'txn_days_cnt_3m', 'txn_max_days_gap_14d',\n    'txn_max_days_gap_1m', 'txn_max_days_gap_3m'\n]\nsubplot_size = (round(len(plot_features) / 2), 2)\nfig_size = (12, 4 * subplot_size[0])\nplt.figure(figsize=fig_size)\n\n\ndef plot_subplot(feature_name: str, subplot_num: int, set_ticks: bool = True):\n    if not set_ticks:\n        xticks = None\n    elif feature_name.split('_')[-1] == \"7d\":\n        xticks=[i for i in range(8)]\n    elif feature_name.split('_')[-1] == \"14d\":\n        xticks=[i * 2 for i in range(8)]\n    elif feature_name.split('_')[-1] == \"1m\":\n        xticks=[i * 3 for i in range(11)]\n    else:\n        xticks=[i * 10 for i in range(10)]\n\n    plt.subplot(*subplot_size, subplot_num)\n    features_df.groupby(feature_name)['client_num'].count().plot(xticks=xticks)\n\n\nfor i, feature_name in enumerate(plot_features):\n    if isinstance(feature_name, tuple):\n        plot_subplot(feature_name[0], i + 1, set_ticks=feature_name[1])\n    else:\n        plot_subplot(feature_name, i + 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:11.913689Z","iopub.execute_input":"2025-07-14T09:30:11.913917Z","iopub.status.idle":"2025-07-14T09:30:12.971325Z","shell.execute_reply.started":"2025-07-14T09:30:11.913900Z","shell.execute_reply":"2025-07-14T09:30:12.970550Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Обучение моделей","metadata":{}},{"cell_type":"code","source":"# Веса классов для WMAE\nCLASS_WEIGHTS = {\n    0: 1.0, 1: 0.72, 2: 0.52, 3: 0.37,\n    4: 0.27, 5: 0.19, 6: 0.14\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:12.972614Z","iopub.execute_input":"2025-07-14T09:30:12.972834Z","iopub.status.idle":"2025-07-14T09:30:12.976845Z","shell.execute_reply.started":"2025-07-14T09:30:12.972818Z","shell.execute_reply":"2025-07-14T09:30:12.976175Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Добавляем веса к таргетам\n\ndf_train['weight'] = df_train['target'].map(CLASS_WEIGHTS)\ndf_train.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:12.977535Z","iopub.execute_input":"2025-07-14T09:30:12.977837Z","iopub.status.idle":"2025-07-14T09:30:13.001363Z","shell.execute_reply.started":"2025-07-14T09:30:12.977814Z","shell.execute_reply":"2025-07-14T09:30:13.000662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Получаем полный датасет для трейна\ntrain_features = features_df.merge(df_train, on='client_num', how='inner')\nprint(\"Train shape:\", train_features.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:13.002084Z","iopub.execute_input":"2025-07-14T09:30:13.002358Z","iopub.status.idle":"2025-07-14T09:30:16.845650Z","shell.execute_reply.started":"2025-07-14T09:30:13.002334Z","shell.execute_reply":"2025-07-14T09:30:16.844859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Делим на обучающую и валидационную выборку с стратификацией по таргету\n# Объём на валидацию - 15%\nX_train, X_val, y_train, y_val, weights_train, weights_val = train_test_split(\n    train_features.drop(columns=['client_num', 'target', 'weight']), \n    train_features['target'], \n    train_features['weight'],\n    test_size=0.15,\n    random_state=42,\n    stratify=train_features['target']\n)\n\nprint(\"Train size:\", X_train.shape)\nprint(\"Validation size:\", X_val.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:16.846512Z","iopub.execute_input":"2025-07-14T09:30:16.846845Z","iopub.status.idle":"2025-07-14T09:30:21.331916Z","shell.execute_reply.started":"2025-07-14T09:30:16.846731Z","shell.execute_reply":"2025-07-14T09:30:21.331336Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Создаем DMatrix с весами\n\ndtrain = xgb.DMatrix(X_train, label=y_train, weight=weights_train)\ndval = xgb.DMatrix(X_val, label=y_val, weight=weights_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:36.190285Z","iopub.execute_input":"2025-07-14T09:30:36.190729Z","iopub.status.idle":"2025-07-14T09:30:40.006566Z","shell.execute_reply.started":"2025-07-14T09:30:36.190700Z","shell.execute_reply":"2025-07-14T09:30:40.006009Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Преобразование в классы с учетом границ\ndef continuous_to_class(preds):\n    # Обеспечиваем попадание в диапазон [0, 7]\n    clipped = np.clip(preds, 0, 6)\n    rounded = np.round(clipped)\n    return rounded.astype(int)\n\n\n# Задаём расчёт метрики для xgb\ndef wmae_xgb(preds, dtrain):\n    labels = dtrain.get_label()\n    weights = dtrain.get_weight()\n\n    # Расчет взвешенной MAE\n    preds_class = continuous_to_class(preds)\n    errors = np.abs(labels - preds_class)\n    wmae = np.mean(weights * errors)\n\n    return 'wmae', wmae","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:30:56.343467Z","iopub.execute_input":"2025-07-14T09:30:56.344421Z","iopub.status.idle":"2025-07-14T09:30:56.349734Z","shell.execute_reply.started":"2025-07-14T09:30:56.344391Z","shell.execute_reply":"2025-07-14T09:30:56.348428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Общие параметры модели, остальные будут подобраны с использованием Optuna\nMODEL_PARAMS = {\n    'objective': 'reg:absoluteerror',\n    \"booster\": \"gbtree\",\n    'eval_metric': 'mae',\n    'tree_method': 'hist',\n    \"device\": \"cuda\"\n}\n# Список различных сидов для обучения модели для получения более стабильных результатов\nSEEDS = [\n    42, 672, 961, 956, 389, \n    387, 438, 379, 755, 173\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:31:00.388148Z","iopub.execute_input":"2025-07-14T09:31:00.388873Z","iopub.status.idle":"2025-07-14T09:31:00.393054Z","shell.execute_reply.started":"2025-07-14T09:31:00.388849Z","shell.execute_reply":"2025-07-14T09:31:00.392168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_batch_models(params, dtrain, dval = None, num_boost_round=1000, verbose_eval=500):\n    \"\"\"\n        Функция для обучения списка моделей с заданными параметрами\n\n        Params:\n            params: Dict[str, Any]\n                Список всех параметров для обучения модели\n            dtrain: xgb.DMatrix\n                Тренировочный набор данных\n            dval: xgb.DMatrix or None, default None\n                Валидационный набор данных\n                Если None, то валидация не проводится\n            num_boost_round: int, default 1000\n                Количество эпох для обучения модели (максимальное)\n            verbose_eval: int, default 500\n                Метрики моделей на эпохах, кратным данному значению, будут логироваться\n        Returns:\n            Возвращается tuple из двух элементов\n            Первый элемент список всех обученных моделей\n            Второй элемент - результирующее значение метрики WMAE на валидации\n            Если dval не был передан, то во втором элементе будет лежать 0\n    \"\"\"\n    models = []\n    predicts = []\n    for seed in SEEDS:\n        params['seed'] = seed\n\n        evals = [(dtrain, 'train')]\n        if dval is not None:\n            evals.append((dval, 'val'))\n    \n        model = xgb.train(\n            params, dtrain, \n            num_boost_round=num_boost_round,\n            evals=evals,\n            custom_metric=wmae_xgb,\n            early_stopping_rounds=50,\n            verbose_eval=verbose_eval\n        )\n        models.append(model)\n\n        if dval is not None:\n            val_preds_cont = model.predict(dval)\n            _, score = wmae_xgb(val_preds_cont, dval)\n            predicts.append(val_preds_cont)\n\n    if predicts:\n        models_pred_cls = continuous_to_class(np.mean(predicts, axis=0))\n        _, res_wmae = wmae_xgb(models_pred_cls, dval)\n    else:\n        res_wmae = 0\n    return models, res_wmae","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:31:02.899918Z","iopub.execute_input":"2025-07-14T09:31:02.900171Z","iopub.status.idle":"2025-07-14T09:31:02.906680Z","shell.execute_reply.started":"2025-07-14T09:31:02.900154Z","shell.execute_reply":"2025-07-14T09:31:02.905859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Обучение моделей с подбором гиперпараметров\ndef objective(trial):\n    params = {\n        # \"booster\": trial.suggest_categorical(\"booster\", [\"gbtree\", \"gblinear\", \"dart\"]),\n        'learning_rate': trial.suggest_float(\"learning_rate\", 0.01, 0.1),\n        \"lambda\": trial.suggest_float(\"lambda\", 1e-5, 1.0, log=True),\n        'gamma': trial.suggest_float(\"gamma\", 1e-5, 1.0, log=True),\n        \"alpha\": trial.suggest_float(\"alpha\", 1e-5, 1.0, log=True),\n        \"subsample\": trial.suggest_float(\"subsample\", 0.5, 1.0),\n        \"colsample_bytree\": trial.suggest_float(\"colsample_bytree\", 0.5, 1.0),\n        'max_depth': trial.suggest_int(\"max_depth\", 4, 10),\n        'min_child_weight': trial.suggest_int(\"min_child_weight\", 10, 100)\n    }\n    params.update(MODEL_PARAMS)\n\n    _, score = train_batch_models(params, dtrain, dval)\n    return score\n\nstudy = optuna.create_study(direction='minimize')\nstudy.optimize(objective, n_trials=100)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study.best_params","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T09:38:21.248161Z","iopub.execute_input":"2025-07-09T09:38:21.248841Z","iopub.status.idle":"2025-07-09T09:38:21.253879Z","shell.execute_reply.started":"2025-07-09T09:38:21.248818Z","shell.execute_reply":"2025-07-09T09:38:21.253319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Задаём оптимальную конфигурацию\n\nparams = {\n    'learning_rate': 0.09231095550711414,\n     'lambda': 0.2104939648855454,\n     'gamma': 0.0011188229126978212,\n     'alpha': 0.06379171385162212,\n     'subsample': 0.5846624679146472,\n     'colsample_bytree': 0.8128808827251961,\n     'max_depth': 10,\n     'min_child_weight': 81\n}\n\nparams.update(MODEL_PARAMS)\n\nmodels, models_wmae = train_batch_models(params, dtrain, dval)\nprint(\"All models wmae:\", models_wmae)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:31:25.921702Z","iopub.execute_input":"2025-07-14T09:31:25.922274Z","iopub.status.idle":"2025-07-14T09:32:00.862172Z","shell.execute_reply.started":"2025-07-14T09:31:25.922250Z","shell.execute_reply":"2025-07-14T09:32:00.861540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Важность признаков\ndef show_models_features_importance(models, num_features=10):\n    rows_per_model = num_features + 3\n    messages = [\"\"] * (rows_per_model * len(models) // 2)\n    \n    for i, model in enumerate(models):\n        importance = model.get_score(importance_type='gain')\n        sorted_importance = sorted(importance.items(), key=lambda x: x[1], reverse=True)\n    \n        indx = rows_per_model * (i // 2)\n        messages[indx] += f\"\\t№{i+1} - Топ {num_features} важных признаков:\\t\"\n    \n        for f_i, (feat, gain) in enumerate(sorted_importance[:num_features]):\n            col_name = train_features.columns[int(feat[1:]) + 1]\n            if len(col_name) < 16:\n                col_name += \"\\t\"\n            messages[indx + f_i + 2] += f\"\\t{col_name}\\t{gain:.4f}\\t\"\n\n    for mes in messages:\n        print(mes)\n\nshow_models_features_importance(models)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:32:05.474067Z","iopub.execute_input":"2025-07-14T09:32:05.474383Z","iopub.status.idle":"2025-07-14T09:32:05.550467Z","shell.execute_reply.started":"2025-07-14T09:32:05.474359Z","shell.execute_reply":"2025-07-14T09:32:05.549786Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Предсказания для теста","metadata":{}},{"cell_type":"code","source":"# Создаём DMatrix для всего набора данных\n\ndtrain_full = xgb.DMatrix(\n    train_features.drop(columns=['client_num', 'target', 'weight']), \n    label=train_features['target'], \n    weight=train_features['weight']\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:32:15.100511Z","iopub.execute_input":"2025-07-14T09:32:15.100782Z","iopub.status.idle":"2025-07-14T09:32:18.571421Z","shell.execute_reply.started":"2025-07-14T09:32:15.100762Z","shell.execute_reply":"2025-07-14T09:32:18.570861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Выбираем фичи по тестовым клиентам и создаём Dmatrix\ntest_features = features_df[features_df['client_num'].isin(test_clients)]\n\ndtest = xgb.DMatrix(\n    test_features.drop(columns=['client_num'])\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:32:23.546390Z","iopub.execute_input":"2025-07-14T09:32:23.546635Z","iopub.status.idle":"2025-07-14T09:32:27.675685Z","shell.execute_reply.started":"2025-07-14T09:32:23.546617Z","shell.execute_reply":"2025-07-14T09:32:27.675120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Обучаем модель на всех данных и прогоняем на тесте\nparams = {\n    'learning_rate': 0.09,\n    'lambda': 0.22,\n    'gamma': 0.002,\n    'alpha': 0.064,\n    'subsample': 0.55,\n    'colsample_bytree': 0.7,\n    'max_depth': 8,\n    'min_child_weight': 90\n}\n\nparams.update(MODEL_PARAMS)\n\nmodels, _ = train_batch_models(\n    params, dtrain_full,\n    # Ограничиваем num_boost_round=1500, чтобы сильно не переобучалась\n    num_boost_round=1500, \n    verbose_eval=1000\n)\n\n# Итоговый prediction как средний предикт по всем обученным моделям\nmodels_pred = [model.predict(dtest) for model in models]\nmodels_pred_cls = continuous_to_class(np.mean(models_pred, axis=0))\n\nsubmission = pd.DataFrame({\n    \"client_num\": test_features['client_num'],\n    \"target\": models_pred_cls\n})\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:32:30.716262Z","iopub.execute_input":"2025-07-14T09:32:30.716817Z","iopub.status.idle":"2025-07-14T09:36:23.976665Z","shell.execute_reply.started":"2025-07-14T09:32:30.716793Z","shell.execute_reply":"2025-07-14T09:36:23.976041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Выведим распределением таргетов на тесте \n# в сравнении с распределением таргетов на обучающей выборке\ntrain_target_proportion = (\n    df_train[\"target\"]\n    .value_counts(normalize=True)\n    .reset_index()\n)\ntrain_target_proportion.columns = [\"target\", \"train_proportion\"]\n\nsubmission_target_proportion = (\n    submission['target']\n    .value_counts(normalize=True)\n    .reset_index()\n)\nsubmission_target_proportion.columns = [\"target\", \"submission_proportion\"]\n\nboth_target_proportion = train_target_proportion.merge(\n    submission_target_proportion, on='target', how='outer'\n).sort_values(\"target\")\n\nboth_target_proportion.plot.bar(\n    x=\"target\", \n    y=[\"train_proportion\", \"submission_proportion\"], \n    rot=0\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:50:46.059818Z","iopub.execute_input":"2025-07-14T09:50:46.060125Z","iopub.status.idle":"2025-07-14T09:50:46.249428Z","shell.execute_reply.started":"2025-07-14T09:50:46.060103Z","shell.execute_reply":"2025-07-14T09:50:46.248573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_models_features_importance(models)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T09:50:55.630633Z","iopub.execute_input":"2025-07-14T09:50:55.631147Z","iopub.status.idle":"2025-07-14T09:50:55.719777Z","shell.execute_reply.started":"2025-07-14T09:50:55.631126Z","shell.execute_reply":"2025-07-14T09:50:55.719006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Сохраняем сабмит, забираем Top-1 на контесте\nsubmission.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:02:57.051035Z","iopub.execute_input":"2025-07-09T13:02:57.051332Z","iopub.status.idle":"2025-07-09T13:02:57.059560Z","shell.execute_reply.started":"2025-07-09T13:02:57.051311Z","shell.execute_reply":"2025-07-09T13:02:57.058681Z"}},"outputs":[],"execution_count":null}]}