{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpu1vmV38","dataSources":[{"sourceId":105399,"databundleVersionId":12733338,"sourceType":"competition"}],"dockerImageVersionId":31042,"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\n!pip install -U optuna\n!pip install -U catboost\n!pip install -U lightgbm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-26T11:43:18.860854Z","iopub.execute_input":"2025-06-26T11:43:18.861543Z","iopub.status.idle":"2025-06-26T11:43:38.351759Z","shell.execute_reply.started":"2025-06-26T11:43:18.861518Z","shell.execute_reply":"2025-06-26T11:43:38.350803Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import polars as pl\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport time\nimport xgboost as xgb\nimport catboost\nimport lightgbm as lgb\nimport optuna\n\nRANDOM_STATE = 42\nnp.random.seed(RANDOM_STATE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-26T11:43:38.353499Z","iopub.execute_input":"2025-06-26T11:43:38.353767Z","iopub.status.idle":"2025-06-26T11:43:39.355931Z","shell.execute_reply.started":"2025-06-26T11:43:38.353743Z","shell.execute_reply":"2025-06-26T11:43:39.355335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load data\ntrain = 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":"2025-06-26T11:43:39.356599Z","iopub.execute_input":"2025-06-26T11:43:39.356872Z","execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Helpers","metadata":{}},{"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.filter(pl.col(\"group\").count().over(\"group\") > 10)\n        .sort([\"group\", \"pred\"], descending=[False, True])\n        .group_by(\"group\", maintain_order=True)\n        .head(3)\n        .group_by(\"group\")\n        .agg(pl.col(\"true\").max())\n        .select(pl.col(\"true\").mean())\n        .item()\n    )","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Engineering","metadata":{}},{"cell_type":"code","source":"df = data_raw.clone()\n\n# More efficient duration to minutes converter\ndef dur_to_min(col):\n    # Extract days and time parts in one pass\n    days = col.str.extract(r\"^(\\d+)\\.\", 1).cast(pl.Int64).fill_null(0) * 1440\n    time_str = pl.when(col.str.contains(r\"^\\d+\\.\")).then(col.str.replace(r\"^\\d+\\.\", \"\")).otherwise(col)\n    hours = time_str.str.extract(r\"^(\\d+):\", 1).cast(pl.Int64).fill_null(0) * 60\n    minutes = time_str.str.extract(r\":(\\d+):\", 1).cast(pl.Int64).fill_null(0)\n    return (days + hours + minutes).fill_null(0)\n\n# Process duration columns\ndur_cols = [\"legs0_duration\", \"legs1_duration\"] + [f\"legs{l}_segments{s}_duration\" for l in (0, 1) for s in (0, 1)]\ndur_exprs = [dur_to_min(pl.col(c)).alias(c) for c in dur_cols if c in df.columns]\n\n# Apply duration transformations first\nif dur_exprs:\n    df = df.with_columns(dur_exprs)\n\n# Precompute marketing carrier columns check\nmc_cols = [f'legs{l}_segments{s}_marketingCarrier_code' for l in (0, 1) for s in range(4)]\nmc_exists = [col for col in mc_cols if col in df.columns]\n\n# Combine all initial transformations\ndf = df.with_columns([\n        # Price features\n        (pl.col(\"totalPrice\") / (pl.col(\"taxes\") + 1)).alias(\"price_per_tax\"),\n        (pl.col(\"taxes\") / (pl.col(\"totalPrice\") + 1)).alias(\"tax_rate\"),\n        pl.col(\"totalPrice\").log1p().alias(\"log_price\"),\n        \n        # Duration features\n        (pl.col(\"legs0_duration\").fill_null(0) + pl.col(\"legs1_duration\").fill_null(0)).alias(\"total_duration\"),\n        pl.when(pl.col(\"legs1_duration\").fill_null(0) > 0)\n            .then(pl.col(\"legs0_duration\") / (pl.col(\"legs1_duration\") + 1))\n            .otherwise(1.0).alias(\"duration_ratio\"),\n        \n        # Trip type\n        (pl.col(\"legs1_duration\").is_null() | \n         (pl.col(\"legs1_duration\") == 0) | \n         pl.col(\"legs1_segments0_departureFrom_airport_iata\").is_null()).cast(pl.Int32).alias(\"is_one_way\"),\n        \n        # Total segments count\n        (pl.sum_horizontal(pl.col(col).is_not_null().cast(pl.UInt8) for col in mc_exists) \n         if mc_exists else pl.lit(0)).alias(\"l0_seg\"),\n        \n        # FF features\n        (pl.col(\"frequentFlyer\").fill_null(\"\").str.count_matches(\"/\") + \n         (pl.col(\"frequentFlyer\").fill_null(\"\") != \"\").cast(pl.Int32)).alias(\"n_ff_programs\"),\n        \n        # Binary features\n        pl.col(\"corporateTariffCode\").is_not_null().cast(pl.Int32).alias(\"has_corporate_tariff\"),\n        (pl.col(\"pricingInfo_isAccessTP\") == 1).cast(pl.Int32).alias(\"has_access_tp\"),\n        \n        # Baggage & fees\n        (pl.col(\"legs0_segments0_baggageAllowance_quantity\").fill_null(0) + \n         pl.col(\"legs1_segments0_baggageAllowance_quantity\").fill_null(0)).alias(\"baggage_total\"),\n        (pl.col(\"miniRules0_monetaryAmount\").fill_null(0) + \n         pl.col(\"miniRules1_monetaryAmount\").fill_null(0)).alias(\"total_fees\"),\n        \n        # Routes & carriers\n        pl.col(\"searchRoute\").is_in([\"MOWLED/LEDMOW\", \"LEDMOW/MOWLED\", \"MOWLED\", \"LEDMOW\", \"MOWAER/AERMOW\"])\n            .cast(pl.Int32).alias(\"is_popular_route\"),\n        \n        # Cabin\n        pl.mean_horizontal([\"legs0_segments0_cabinClass\", \"legs1_segments0_cabinClass\"]).alias(\"avg_cabin_class\"),\n        (pl.col(\"legs0_segments0_cabinClass\").fill_null(0) - \n         pl.col(\"legs1_segments0_cabinClass\").fill_null(0)).alias(\"cabin_class_diff\"),\n])\n\n# Segment counts - more efficient\nseg_exprs = []\nfor leg in (0, 1):\n    seg_cols = [f\"legs{leg}_segments{s}_duration\" for s in range(4) if f\"legs{leg}_segments{s}_duration\" in df.columns]\n    if seg_cols:\n        seg_exprs.append(\n            pl.sum_horizontal(pl.col(c).is_not_null() for c in seg_cols)\n                .cast(pl.Int32).alias(f\"n_segments_leg{leg}\")\n        )\n    else:\n        seg_exprs.append(pl.lit(0).cast(pl.Int32).alias(f\"n_segments_leg{leg}\"))\n\n# Add segment-based features\n# First create segment counts\ndf = df.with_columns(seg_exprs)\n\n# Then use them for derived features\ndf = df.with_columns([\n    (pl.col(\"n_segments_leg0\") + pl.col(\"n_segments_leg1\")).alias(\"total_segments\"),\n    (pl.col(\"n_segments_leg0\") == 1).cast(pl.Int32).alias(\"is_direct_leg0\"),\n    pl.when(pl.col(\"is_one_way\") == 1).then(0)\n        .otherwise((pl.col(\"n_segments_leg1\") == 1).cast(pl.Int32)).alias(\"is_direct_leg1\"),\n])\n\n# More derived features\ndf = df.with_columns([\n    (pl.col(\"is_direct_leg0\") & pl.col(\"is_direct_leg1\")).cast(pl.Int32).alias(\"both_direct\"),\n    ((pl.col(\"isVip\") == 1) | (pl.col(\"n_ff_programs\") > 0)).cast(pl.Int32).alias(\"is_vip_freq\"),\n    (pl.col(\"baggage_total\") > 0).cast(pl.Int32).alias(\"has_baggage\"),\n    (pl.col(\"total_fees\") > 0).cast(pl.Int32).alias(\"has_fees\"),\n    (pl.col(\"total_fees\") / (pl.col(\"totalPrice\") + 1)).alias(\"fee_rate\"),\n    pl.col(\"Id\").count().over(\"ranker_id\").alias(\"group_size\"),\n])\n\n# Add major carrier flag if column exists\nif \"legs0_segments0_marketingCarrier_code\" in df.columns:\n    df = df.with_columns(\n        pl.col(\"legs0_segments0_marketingCarrier_code\").is_in([\"SU\", \"S7\", \"U6\"])\n            .cast(pl.Int32).alias(\"is_major_carrier\")\n    )\nelse:\n    df = df.with_columns(pl.lit(0).alias(\"is_major_carrier\"))\n\ndf = df.with_columns(pl.col(\"group_size\").log1p().alias(\"group_size_log\"))\n\n# Time features - batch process\ntime_exprs = []\nfor col in (\"legs0_departureAt\", \"legs0_arrivalAt\", \"legs1_departureAt\", \"legs1_arrivalAt\"):\n    if col in df.columns:\n        dt = pl.col(col).str.to_datetime(strict=False)\n        h = dt.dt.hour().fill_null(12)\n        time_exprs.extend([\n            h.alias(f\"{col}_hour\"),\n            dt.dt.weekday().fill_null(0).alias(f\"{col}_weekday\"),\n            (((h >= 6) & (h <= 9)) | ((h >= 17) & (h <= 20))).cast(pl.Int32).alias(f\"{col}_business_time\")\n        ])\nif time_exprs:\n    df = df.with_columns(time_exprs)\n\n# Batch rank computations - more efficient with single pass\n# First apply the columns that will be used for ranking\ndf = df.with_columns([\n    pl.col(\"group_size\").log1p().alias(\"group_size_log\"),\n])\n\n# Price and duration basic ranks\nrank_exprs = []\nfor col, alias in [(\"totalPrice\", \"price\"), (\"total_duration\", \"duration\")]:\n    rank_exprs.append(pl.col(col).rank().over(\"ranker_id\").alias(f\"{alias}_rank\"))\n\n# Price-specific features\nprice_exprs = [\n    (pl.col(\"totalPrice\").rank(\"average\").over(\"ranker_id\") / \n     pl.col(\"totalPrice\").count().over(\"ranker_id\")).alias(\"price_pct_rank\"),\n    (pl.col(\"totalPrice\") == pl.col(\"totalPrice\").min().over(\"ranker_id\")).cast(pl.Int32).alias(\"is_cheapest\"),\n    ((pl.col(\"totalPrice\") - pl.col(\"totalPrice\").median().over(\"ranker_id\")) / \n     (pl.col(\"totalPrice\").std().over(\"ranker_id\") + 1)).alias(\"price_from_median\"),\n    (pl.col(\"l0_seg\") == pl.col(\"l0_seg\").min().over(\"ranker_id\")).cast(pl.Int32).alias(\"is_min_segments\"),\n]\n\n# Apply initial ranks\ndf = df.with_columns(rank_exprs + price_exprs)\n\n# Cheapest direct - more efficient\ndirect_cheapest = (\n    df.filter(pl.col(\"is_direct_leg0\") == 1)\n    .group_by(\"ranker_id\")\n    .agg(pl.col(\"totalPrice\").min().alias(\"min_direct\"))\n)\n\ndf = df.join(direct_cheapest, on=\"ranker_id\", how=\"left\").with_columns(\n    ((pl.col(\"is_direct_leg0\") == 1) & \n     (pl.col(\"totalPrice\") == pl.col(\"min_direct\"))).cast(pl.Int32).fill_null(0).alias(\"is_direct_cheapest\")\n).drop(\"min_direct\")","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Fill nulls\ndata = df.with_columns(\n    [pl.col(c).fill_null(0) for c in df.select(pl.selectors.numeric()).columns] +\n    [pl.col(c).fill_null(\"missing\") for c in df.select(pl.selectors.string()).columns]\n)","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Selection","metadata":{}},{"cell_type":"code","source":"# Categorical features\ncat_features = [\n    'nationality', 'searchRoute', 'corporateTariffCode',\n    'bySelf', 'sex', 'companyID',\n    # Leg 0 segments 0-1\n    'legs0_segments0_aircraft_code', 'legs0_segments0_arrivalTo_airport_city_iata',\n    'legs0_segments0_arrivalTo_airport_iata', 'legs0_segments0_departureFrom_airport_iata',\n    'legs0_segments0_marketingCarrier_code', 'legs0_segments0_operatingCarrier_code',\n    'legs0_segments0_flightNumber',\n    'legs0_segments1_aircraft_code', 'legs0_segments1_arrivalTo_airport_city_iata',\n    'legs0_segments1_arrivalTo_airport_iata', 'legs0_segments1_departureFrom_airport_iata',\n    'legs0_segments1_marketingCarrier_code', 'legs0_segments1_operatingCarrier_code',\n    'legs0_segments1_flightNumber',\n    # Leg 1 segments 0-1\n    'legs1_segments0_aircraft_code', 'legs1_segments0_arrivalTo_airport_city_iata',\n    'legs1_segments0_arrivalTo_airport_iata', 'legs1_segments0_departureFrom_airport_iata',\n    'legs1_segments0_marketingCarrier_code', 'legs1_segments0_operatingCarrier_code',\n    'legs1_segments0_flightNumber',\n    'legs1_segments1_aircraft_code', 'legs1_segments1_arrivalTo_airport_city_iata',\n    'legs1_segments1_arrivalTo_airport_iata', 'legs1_segments1_departureFrom_airport_iata',\n    'legs1_segments1_marketingCarrier_code', 'legs1_segments1_operatingCarrier_code',\n    'legs1_segments1_flightNumber',\n]\n\n# Columns to exclude (uninformative or problematic)\nexclude_cols = [\n    'Id', 'ranker_id', 'selected', 'profileId', 'requestDate',\n    'legs0_departureAt', 'legs0_arrivalAt', 'legs1_departureAt', 'legs1_arrivalAt',\n    'miniRules0_percentage', 'miniRules1_percentage',  # >90% missing\n    'frequentFlyer',  # Already processed\n    # Exclude constant columns\n    'pricingInfo_passengerCount'\n]\n\n\n# Exclude segment 2-3 columns (>98% missing)\nfor leg in [0, 1]:\n    for seg in [2, 3]:\n        for suffix in ['aircraft_code', 'arrivalTo_airport_city_iata', 'arrivalTo_airport_iata',\n                      'baggageAllowance_quantity', 'baggageAllowance_weightMeasurementType',\n                      'cabinClass', 'departureFrom_airport_iata', 'duration', 'flightNumber',\n                      'marketingCarrier_code', 'operatingCarrier_code', 'seatsAvailable']:\n            exclude_cols.append(f'legs{leg}_segments{seg}_{suffix}')\n\nfeature_cols = [col for col in data.columns if col not in exclude_cols]\ncat_features_final = [col for col in cat_features if col in feature_cols]\n\nprint(f\"Using {len(feature_cols)} features ({len(cat_features_final)} categorical)\")\n\nX = data.select(feature_cols)\ny = data.select('selected')\ngroups = data.select('ranker_id')","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Training and Tuning","metadata":{}},{"cell_type":"markdown","source":"### 1. XGBoost Model","metadata":{}},{"cell_type":"code","source":"data_xgb = X.with_columns([(pl.col(c).rank(\"dense\") - 1).fill_null(-1).cast(pl.Int32) for c in cat_features_final])\n\nn1 = 16487352 # split train to train and val (10%) in time\nn2 = train.height\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').agg(pl.len()).sort('ranker_id')['len'].to_numpy()\ngroup_sizes_va = groups_va.group_by('ranker_id').agg(pl.len()).sort('ranker_id')['len'].to_numpy()\ngroup_sizes_te = groups_te.group_by('ranker_id').agg(pl.len()).sort('ranker_id')['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":{"execution_failed":"2025-06-26T11:44:42.429Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CODE CELL\nfinal_xgb_params = {'objective': 'rank:pairwise', 'eval_metric': 'ndcg@3', \n                    'max_depth': 8, 'min_child_weight': 14, 'subsample': 0.9, \n                    'colsample_bytree': 1.0, 'lambda': 3.5330891736457763 , \n                    'learning_rate': 0.0521879929228514 ,\n                    'seed': RANDOM_STATE, 'n_jobs': -1}\nfinal_xgb_params.update(final_xgb_params)\n\nprint(\"\\nTraining final XGBoost model with optimized parameters...\")\nxgb_model = xgb.train(\n    final_xgb_params, dtrain,\n    num_boost_round=1500,\n    evals=[(dtrain, 'train'), (dval, 'val')],\n    early_stopping_rounds=100,\n    verbose_eval=50\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3. LightGBM Model","metadata":{}},{"cell_type":"code","source":"# CODE CELL\n# LightGBM requires its own Dataset object. We can reuse the rank-encoded data from XGBoost.\nprint(\"Creating LightGBM Datasets...\")\nlgb_train = lgb.Dataset(\n    data=data_xgb_tr, \n    label=y_tr.to_numpy().flatten(), \n    group=group_sizes_tr,\n    feature_name=feature_cols,\n    free_raw_data=False\n)\n\nlgb_val = lgb.Dataset(\n    data=data_xgb_va, \n    label=y_va.to_numpy().flatten(), \n    group=group_sizes_va,\n    feature_name=feature_cols,\n    reference=lgb_train,\n    free_raw_data=False\n)\nprint(\"LightGBM Datasets created successfully.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CODE CELL\nfinal_lgb_params = {\n    'objective': 'lambdarank', 'metric': 'ndcg', 'boosting_type': 'gbdt','eval_at': [3],\n    'num_leaves': 137, 'learning_rate': 0.19236092380700556, 'min_child_samples': 69, \n    'lambda_l1': 0.001786334561662628, 'lambda_l2': 7.881799636447006, \n    'feature_fraction': 0.6015465928218717, 'bagging_fraction': 0.8535794374747682, \n    'bagging_freq': 7, 'n_jobs': -1, 'random_state': RANDOM_STATE, 'label_gain': [0, 1]\n}\nfinal_lgb_params.update(final_lgb_params)\n\nprint(\"\\nTraining final LightGBM model with optimized parameters...\")\nlgb_model = lgb.train(\n    final_lgb_params,\n    lgb_train,\n    num_boost_round=1500,\n    valid_sets=[lgb_train, lgb_val],\n    callbacks=[lgb.early_stopping(100), lgb.log_evaluation(50)]\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n--- Training LightGBM DART Model ---\")\n\ndart_params = {\n    'objective': 'lambdarank', 'metric': 'ndcg', 'eval_at': [3],\n    'boosting_type': 'dart', \n    'n_estimators': 2500,     \n    'learning_rate': 0.04,\n    'num_leaves': 50,\n    'drop_rate': 0.1,        \n    'subsample': 0.8,\n    'skip_drop': 0.5,        \n    'n_jobs': -1,\n    'random_state': RANDOM_STATE,\n    'label_gain': [0, 1]\n}\n\nlgb_model_dart = lgb.train(\n    dart_params,\n    lgb_train, \n    num_boost_round=dart_params['n_estimators'], \n    valid_sets=[lgb_val],\n    callbacks=[lgb.early_stopping(100), lgb.log_evaluation(50)]\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Blending and Final Evaluation","metadata":{}},{"cell_type":"code","source":"# CODE CELL\nprint(\"Evaluating all models on the validation set...\")\n\n# Get predictions from all three models\nxgb_va_preds = xgb_model.predict(dval)\nlgb_va_preds = lgb_model.predict(data_xgb_va)\nlgb_dart_va_preds = lgb_model_dart.predict(data_xgb_va)\n\n# --- Blending using Rank Averaging ---\n# Create a dataframe with all scores\nval_scores_df = pl.DataFrame({\n    \"group\": groups_va['ranker_id'],\n    \"true\": y_va['selected'],\n    \"xgb_score\": xgb_va_preds,\n    \"lgb_gbdt_score\": lgb_va_preds,\n    \"lgb_dart_score\": lgb_dart_va_preds\n})\n\n# Calculate ranks for each model\nval_scores_df = val_scores_df.with_columns(\n    pl.col(\"xgb_score\").rank(method=\"average\", descending=True).over(\"group\").alias(\"xgb_rank\"),\n    pl.col(\"lgb_gbdt_score\").rank(method=\"average\", descending=True).over(\"group\").alias(\"lgb_gbdt_rank\"),\n    pl.col(\"lgb_dart_score\").rank(method=\"average\", descending=True).over(\"group\").alias(\"lgb_dart_rank\")\n)\n\n# Create a weighted blend of the ranks (lower is better)\n# Assign higher weight to the best performing models on validation\nval_scores_df = val_scores_df.with_columns(\n    (0.6 * pl.col(\"xgb_rank\") + 0.2 * pl.col(\"lgb_gbdt_rank\") + 0.2 * pl.col(\"lgb_dart_rank\")).alias(\"blend_score\")\n)\n\n# Calculate HitRate@3 for each model and the blend\nxgb_hr3 = hitrate_at_3(y_va['selected'], xgb_va_preds, groups_va['ranker_id'])\nlgb_hr3 = hitrate_at_3(y_va['selected'], lgb_va_preds, groups_va['ranker_id'])\nlgb_dart_hr3 = hitrate_at_3(y_va['selected'], lgb_dart_va_preds, groups_va['ranker_id'])\nblend_hr3 = hitrate_at_3(val_scores_df['true'], -val_scores_df['blend_score'], val_scores_df['group']) # Negate score as lower is better\n\nprint(\"-\" * 30)\nprint(f\"XGBoost HitRate@3:     {xgb_hr3:.4f}\")\nprint(f\"LGBM GBDT HitRate@3:   {lgb_hr3:.4f}\")\nprint(f\"LGBM DART HitRate@3:   {lgb_dart_hr3:.4f}\")\nprint(\"-\" * 30)\nprint(f\"3-Model Blend HitRate@3: {blend_hr3:.4f}\")\nprint(\"-\" * 30)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Submission","metadata":{}},{"cell_type":"code","source":"# CODE CELL\nprint(\"Generating predictions for the test set with all three models...\")\n\n# Predict with all models\nxgb_test_preds = xgb_model.predict(dtest)\nlgb_gbdt_test_preds = lgb_model.predict(data_xgb_te)\nlgb_dart_test_preds = lgb_model_dart.predict(data_xgb_te)\n\n# Create submission file using blended scores\nsubmission_df = test.select(['Id', 'ranker_id']).with_columns(\n    pl.Series('xgb_score', xgb_test_preds),\n    pl.Series('lgb_gbdt_score', lgb_gbdt_test_preds),\n    pl.Series('lgb_dart_score', lgb_dart_test_preds)\n).with_columns(\n    pl.col(\"xgb_score\").rank(method=\"average\", descending=True).over(\"ranker_id\").alias(\"xgb_rank\"),\n    pl.col(\"lgb_gbdt_score\").rank(method=\"average\", descending=True).over(\"ranker_id\").alias(\"lgb_gbdt_rank\"),\n    pl.col(\"lgb_dart_score\").rank(method=\"average\", descending=True).over(\"ranker_id\").alias(\"lgb_dart_rank\")\n).with_columns(\n    (0.5 * pl.col(\"xgb_rank\") + 0.2 * pl.col(\"lgb_gbdt_rank\") + 0.3 * pl.col(\"lgb_dart_rank\")).alias(\"blend_score\")\n).with_columns(\n    pl.col('blend_score').rank(method='ordinal', descending=False).over('ranker_id').cast(pl.Int32).alias('selected')\n).select(['Id', 'ranker_id', 'selected'])\n\nsubmission_df.write_csv('submission.csv')\n\nprint(\"\\nSubmission file 'submission.csv' created successfully.\")\nprint(submission_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}