{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpuV5e8","dataSources":[{"sourceId":105399,"databundleVersionId":12733338,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"%%capture\n!pip install -U xgboost\n!pip install -U polars","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:27:42.703550Z","iopub.execute_input":"2026-02-08T19:27:42.703706Z","iopub.status.idle":"2026-02-08T19:28:13.300522Z","shell.execute_reply.started":"2026-02-08T19:27:42.703688Z","shell.execute_reply":"2026-02-08T19:28:13.299346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import polars as pl\nimport numpy as np\nimport pandas as pd\nimport xgboost as xgb\nfrom sklearn.model_selection import train_test_split\nfrom collections import defaultdict\nimport random\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:13.301287Z","iopub.execute_input":"2026-02-08T19:28:13.301460Z","iopub.status.idle":"2026-02-08T19:28:16.000761Z","shell.execute_reply.started":"2026-02-08T19:28:13.301442Z","shell.execute_reply":"2026-02-08T19:28:15.999613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:16.001365Z","iopub.execute_input":"2026-02-08T19:28:16.001642Z","iopub.status.idle":"2026-02-08T19:28:16.004754Z","shell.execute_reply.started":"2026-02-08T19:28:16.001626Z","shell.execute_reply":"2026-02-08T19:28:16.003945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def hitrate_at_3(y_true, y_pred, groups):\n    df = pl.DataFrame({\n        \"group\": groups,\n        \"pred\": y_pred,\n        \"true\": y_true\n    })\n\n    return (\n        df.sort([\"group\", \"pred\"], descending=[False, True])\n        .group_by(\"group\")\n        .head(3)\n        .group_by(\"group\")\n        .agg(pl.col(\"true\").max())\n        .select(pl.col(\"true\").mean())\n        .item()\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:16.005233Z","iopub.execute_input":"2026-02-08T19:28:16.005416Z","iopub.status.idle":"2026-02-08T19:28:16.920104Z","shell.execute_reply.started":"2026-02-08T19:28:16.005402Z","shell.execute_reply":"2026-02-08T19:28:16.919029Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def dur_to_min(col):\n    days = col.str.extract(r\"^(\\d+)\\.\", 1).cast(pl.Int64).fill_null(0) * 1440\n    time = col.str.replace(r\"^\\d+\\.\", \"\")\n    hours = time.str.extract(r\"^(\\d+):\", 1).cast(pl.Int64).fill_null(0) * 60\n    mins = time.str.extract(r\":(\\d+):\", 1).cast(pl.Int64).fill_null(0)\n\n    return (days + hours + mins).fill_null(0)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:16.920607Z","iopub.execute_input":"2026-02-08T19:28:16.920763Z","iopub.status.idle":"2026-02-08T19:28:16.935686Z","shell.execute_reply.started":"2026-02-08T19:28:16.920748Z","shell.execute_reply":"2026-02-08T19:28:16.934890Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pl.read_parquet('/kaggle/input/aeroclub-recsys-2025/train.parquet').drop('__index_level_0__')\ntest = pl.read_parquet('/kaggle/input/aeroclub-recsys-2025/test.parquet').drop('__index_level_0__').with_columns(pl.lit(0, dtype=pl.Int64).alias(\"selected\"))\n\ndata_raw = pl.concat((train, test))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:16.936175Z","iopub.execute_input":"2026-02-08T19:28:16.936353Z","iopub.status.idle":"2026-02-08T19:28:20.358218Z","shell.execute_reply.started":"2026-02-08T19:28:16.936339Z","shell.execute_reply":"2026-02-08T19:28:20.357150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = data_raw.clone()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:20.358998Z","iopub.execute_input":"2026-02-08T19:28:20.359176Z","iopub.status.idle":"2026-02-08T19:28:20.362045Z","shell.execute_reply.started":"2026-02-08T19:28:20.359159Z","shell.execute_reply":"2026-02-08T19:28:20.361244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for c in [\"legs0_duration\", \"legs1_duration\"]:\n    if c in df.columns:\n        df = df.with_columns(dur_to_min(pl.col(c)).alias(c))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:20.362577Z","iopub.execute_input":"2026-02-08T19:28:20.362734Z","iopub.status.idle":"2026-02-08T19:28:35.693481Z","shell.execute_reply.started":"2026-02-08T19:28:20.362720Z","shell.execute_reply":"2026-02-08T19:28:35.692385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = df.with_columns([\n\n    pl.col(\"totalPrice\").log1p().alias(\"log_price\"),\n\n    (pl.col(\"totalPrice\") / (pl.col(\"taxes\") + 1)).alias(\"price_per_tax\"),\n\n    (pl.col(\"legs0_duration\").fill_null(0) +\n     pl.col(\"legs1_duration\").fill_null(0)).alias(\"total_duration\"),\n\n    pl.col(\"corporateTariffCode\").is_not_null().cast(pl.Int32).alias(\"has_corporate\"),\n\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:35.694199Z","iopub.execute_input":"2026-02-08T19:28:35.694375Z","iopub.status.idle":"2026-02-08T19:28:35.909582Z","shell.execute_reply.started":"2026-02-08T19:28:35.694360Z","shell.execute_reply":"2026-02-08T19:28:35.908536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if \"legs1_duration\" in df.columns:\n    df = df.with_columns(\n        (pl.col(\"legs1_duration\").is_null() | (pl.col(\"legs1_duration\") == 0))\n        .cast(pl.Int32)\n        .alias(\"is_one_way\")\n    )\nelse:\n    df = df.with_columns(pl.lit(1).alias(\"is_one_way\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:35.910059Z","iopub.execute_input":"2026-02-08T19:28:35.910209Z","iopub.status.idle":"2026-02-08T19:28:35.946999Z","shell.execute_reply.started":"2026-02-08T19:28:35.910195Z","shell.execute_reply":"2026-02-08T19:28:35.945914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seg_cols = [c for c in df.columns if c.startswith(\"legs0_segments\") and c.endswith(\"_duration\")]\n\nif seg_cols:\n    df = df.with_columns(\n        pl.sum_horizontal(pl.col(c).is_not_null() for c in seg_cols)\n        .cast(pl.Int32)\n        .alias(\"n_segments_leg0\")\n    )\nelse:\n    df = df.with_columns(pl.lit(1).alias(\"n_segments_leg0\"))\n\ndf = df.with_columns([\n    (pl.col(\"n_segments_leg0\") == 1).cast(pl.Int32).alias(\"is_direct\"),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:35.947669Z","iopub.execute_input":"2026-02-08T19:28:35.947849Z","iopub.status.idle":"2026-02-08T19:28:36.099573Z","shell.execute_reply.started":"2026-02-08T19:28:35.947833Z","shell.execute_reply":"2026-02-08T19:28:36.098554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target = \"selected\"\ngroup_col = \"ranker_id\"\n\ncategoricals = [\n    \"nationality\",\n    \"searchRoute\",\n    \"legs0_segments0_marketingCarrier_code\",\n]\n\nnumerics = [\n    \"log_price\",\n    \"price_per_tax\",\n    \"total_duration\",\n    \"has_corporate\",\n    \"is_one_way\",\n    \"n_segments_leg0\",\n    \"is_direct\",\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:36.100159Z","iopub.execute_input":"2026-02-08T19:28:36.100344Z","iopub.status.idle":"2026-02-08T19:28:36.103195Z","shell.execute_reply.started":"2026-02-08T19:28:36.100328Z","shell.execute_reply":"2026-02-08T19:28:36.102494Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"categoricals = [c for c in categoricals if c in df.columns]\nnumerics = [c for c in numerics if c in df.columns]\n\nfeature_cols = categoricals + numerics\n\nX = df.select(feature_cols)\ny = df.select(target)\ngroups = df.select(group_col)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:36.103835Z","iopub.execute_input":"2026-02-08T19:28:36.103994Z","iopub.status.idle":"2026-02-08T19:28:36.113851Z","shell.execute_reply.started":"2026-02-08T19:28:36.103979Z","shell.execute_reply":"2026-02-08T19:28:36.112787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"data_xgb = X.with_columns([(pl.col(c).rank(\"dense\") - 1).fill_null(-1).cast(pl.Int32) for c in categoricals])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:36.114390Z","iopub.execute_input":"2026-02-08T19:28:36.114549Z","iopub.status.idle":"2026-02-08T19:28:37.232628Z","shell.execute_reply.started":"2026-02-08T19:28:36.114534Z","shell.execute_reply":"2026-02-08T19:28:37.231546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"type(data_xgb)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:37.233241Z","iopub.execute_input":"2026-02-08T19:28:37.233428Z","iopub.status.idle":"2026-02-08T19:28:37.238900Z","shell.execute_reply.started":"2026-02-08T19:28:37.233413Z","shell.execute_reply":"2026-02-08T19:28:37.238127Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"n1 = int(data_xgb.height * 0.90)\nn2 = int(data_xgb.height)\n\ndata_xgb_tr, data_xgb_va, data_xgb_te = data_xgb[:n1], data_xgb[n1:n2], data_xgb[n2:]\ny_tr, y_va, y_te = y[:n1], y[n1:n2], y[n2:]\ngroups_tr, groups_va, groups_te = groups[:n1], groups[n1:n2], groups[n2:]\n\ngroup_sizes_tr = groups_tr.group_by('ranker_id', maintain_order=True).agg(pl.len())['len'].to_numpy()\ngroup_sizes_va = groups_va.group_by('ranker_id', maintain_order=True).agg(pl.len())['len'].to_numpy()\ngroup_sizes_te = groups_te.group_by('ranker_id', maintain_order=True).agg(pl.len())['len'].to_numpy()\ndtrain = xgb.DMatrix(data_xgb_tr, label=y_tr, group=group_sizes_tr, feature_names=data_xgb.columns)\ndval   = xgb.DMatrix(data_xgb_va, label=y_va, group=group_sizes_va, feature_names=data_xgb.columns)\ndtest  = xgb.DMatrix(data_xgb_te, label=y_te, group=group_sizes_te, feature_names=data_xgb.columns)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:37.239380Z","iopub.execute_input":"2026-02-08T19:28:37.239532Z","iopub.status.idle":"2026-02-08T19:28:39.200002Z","shell.execute_reply.started":"2026-02-08T19:28:37.239518Z","shell.execute_reply":"2026-02-08T19:28:39.198881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"params = {\n    \"objective\": \"rank:pairwise\",\n    \"eval_metric\": \"ndcg@3\",\n    \"max_depth\": 10,\n    \"learning_rate\": 0.05,\n    \"subsample\": 0.8,\n    \"colsample_bytree\": 0.8,\n    \"lambda\": 10.0,\n    \"seed\": 42,\n}\n\nprint(\"Training model...\")\n\nmodel = xgb.train(\n    params,\n    dtrain,\n    num_boost_round=1000,\n    evals=[(dtrain, \"train\"), (dval, \"val\")],\n    verbose_eval=50,\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:28:39.200604Z","iopub.execute_input":"2026-02-08T19:28:39.200782Z","iopub.status.idle":"2026-02-08T19:33:52.862193Z","shell.execute_reply.started":"2026-02-08T19:28:39.200765Z","shell.execute_reply":"2026-02-08T19:33:52.861315Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Evaluate XGBoost\nxgb_va_preds = model.predict(dval)\nxgb_hr3 = hitrate_at_3(y_va, xgb_va_preds, groups_va)\nprint(f\"HitRate@3: {xgb_hr3:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T19:33:52.862742Z","iopub.execute_input":"2026-02-08T19:33:52.862917Z","iopub.status.idle":"2026-02-08T19:33:53.879406Z","shell.execute_reply.started":"2026-02-08T19:33:52.862901Z","shell.execute_reply":"2026-02-08T19:33:53.878144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}