{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.18","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":31091,"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 lightgbm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:18:10.481073Z","iopub.execute_input":"2025-08-15T13:18:10.481258Z","iopub.status.idle":"2025-08-15T13:18:30.604194Z","shell.execute_reply.started":"2025-08-15T13:18:10.481238Z","shell.execute_reply":"2025-08-15T13:18:30.598587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport polars as pl\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport time\nimport xgboost as xgb\n\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:18:30.605405Z","iopub.execute_input":"2025-08-15T13:18:30.605626Z","iopub.status.idle":"2025-08-15T13:18:34.718923Z","shell.execute_reply.started":"2025-08-15T13:18:30.605601Z","shell.execute_reply":"2025-08-15T13:18:34.714131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"RANDOM_STATE = 42\nnp.random.seed(RANDOM_STATE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:18:34.721022Z","iopub.execute_input":"2025-08-15T13:18:34.721344Z","iopub.status.idle":"2025-08-15T13:18:34.729032Z","shell.execute_reply.started":"2025-08-15T13:18:34.721323Z","shell.execute_reply":"2025-08-15T13:18:34.725244Z"}},"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-08-15T13:18:34.732456Z","iopub.execute_input":"2025-08-15T13:18:34.732705Z","iopub.status.idle":"2025-08-15T13:18:39.116531Z","shell.execute_reply.started":"2025-08-15T13:18:34.732683Z","shell.execute_reply":"2025-08-15T13:18:39.112078Z"}},"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.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":{"iopub.status.busy":"2025-08-15T13:18:39.118446Z","iopub.execute_input":"2025-08-15T13:18:39.118707Z","iopub.status.idle":"2025-08-15T13:18:39.128503Z","shell.execute_reply.started":"2025-08-15T13:18:39.118683Z","shell.execute_reply":"2025-08-15T13:18:39.124199Z"}},"outputs":[],"execution_count":null},{"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\")\n\n# Popularity features - efficient join\ndf = (\n    df.join(\n        train.group_by('legs0_segments0_marketingCarrier_code').agg(pl.mean('selected').alias('carrier0_pop')),\n        on='legs0_segments0_marketingCarrier_code', \n        how='left'\n    )\n    .join(\n        train.group_by('legs1_segments0_marketingCarrier_code').agg(pl.mean('selected').alias('carrier1_pop')),\n        on='legs1_segments0_marketingCarrier_code', \n        how='left'\n    )\n    .with_columns([\n        pl.col('carrier0_pop').fill_null(0.0),\n        pl.col('carrier1_pop').fill_null(0.0),\n    ])\n)\n\n# Final features including popularity\ndf = df.with_columns([\n    (pl.col('carrier0_pop') * pl.col('carrier1_pop')).alias('carrier_pop_product'),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:18:39.130622Z","iopub.execute_input":"2025-08-15T13:18:39.130841Z","iopub.status.idle":"2025-08-15T13:19:45.526093Z","shell.execute_reply.started":"2025-08-15T13:18:39.130821Z","shell.execute_reply":"2025-08-15T13:19:45.520789Z"}},"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":{"iopub.status.busy":"2025-08-15T13:19:45.528104Z","iopub.execute_input":"2025-08-15T13:19:45.528355Z","iopub.status.idle":"2025-08-15T13:19:47.834287Z","shell.execute_reply.started":"2025-08-15T13:19:45.528332Z","shell.execute_reply":"2025-08-15T13:19:47.828173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Categorical features\ncat_features = [\n    'nationality', 'searchRoute', 'corporateTariffCode','route',\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":{"iopub.status.busy":"2025-08-15T13:19:47.836634Z","iopub.execute_input":"2025-08-15T13:19:47.836955Z","iopub.status.idle":"2025-08-15T13:19:47.867108Z","shell.execute_reply.started":"2025-08-15T13:19:47.836928Z","shell.execute_reply":"2025-08-15T13:19:47.850473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import GroupKFold\nimport lightgbm as lgb  # --- NEW CODE ---\n\ndata_xgb = X.with_columns([(pl.col(c).rank(\"dense\") - 1).fill_null(-1).cast(pl.Int32) for c in cat_features_final])\n\n# Initialize GroupKFold (unchanged)\ngkf = GroupKFold(n_splits=5)\ntrain_idx, val_idx = next(gkf.split(data_xgb[:train.height], y[:train.height], groups=groups[\"ranker_id\"][:train.height]))\n\n# Split data (unchanged)\ndata_xgb_tr, data_xgb_va = data_xgb[train_idx], data_xgb[val_idx]\ny_tr, y_va = y[train_idx], y[val_idx]\ngroups_tr, groups_va = groups[train_idx], groups[val_idx]\ndata_xgb_te = data_xgb[train.height:]\ny_te = y[train.height:]\ngroups_te = groups[train.height:]\n\n# --- NEW CODE: Prepare LightGBM data ---\nlgb_train = lgb.Dataset(\n    data_xgb_tr.to_pandas(), \n    label=y_tr.to_numpy().ravel(),\n    group=groups_tr.group_by('ranker_id').agg(pl.len())['len'].to_numpy(),\n    categorical_feature=cat_features_final\n)\n\n# XGBoost DMatrices (unchanged)\ngroup_sizes_tr = groups_tr.group_by('ranker_id').agg(pl.len())['len'].to_numpy()\ngroup_sizes_va = groups_va.group_by('ranker_id').agg(pl.len())['len'].to_numpy()\ngroup_sizes_te = groups_te.group_by('ranker_id').agg(pl.len())['len'].to_numpy()\n\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":"2025-08-15T13:19:47.870260Z","iopub.execute_input":"2025-08-15T13:19:47.870538Z","iopub.status.idle":"2025-08-15T13:20:55.910449Z","shell.execute_reply.started":"2025-08-15T13:19:47.870515Z","shell.execute_reply":"2025-08-15T13:20:55.903868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xgb_params = {\n    'objective': 'rank:pairwise',\n    'eval_metric': 'ndcg@3',\n    'learning_rate': 0.05,  # Increased from 0.022\n    'max_depth': 8,         # Reduced from 14 to prevent overfitting\n    'min_child_weight': 3,\n    'subsample': 0.8,\n    'colsample_bytree': 0.6,\n    'gamma': 1.0,\n    'lambda': 5.0,\n    'alpha': 0.5,\n    'seed': RANDOM_STATE,\n    'n_jobs': -1,\n    'early_stopping_rounds': 50,\n    'max_bin': 256,         # Added for better numerical stability\n    'tree_method': 'hist'   # Faster training\n}\n\nprint(\"Training XGBoost...\")\nxgb_model = xgb.train(\n    xgb_params,\n    dtrain,\n    num_boost_round=1250,\n    evals=[(dtrain, 'train'), (dval, 'val')],\n    early_stopping_rounds=100,\n    verbose_eval=50\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:20:55.912548Z","iopub.execute_input":"2025-08-15T13:20:55.912881Z","iopub.status.idle":"2025-08-15T13:36:42.836296Z","shell.execute_reply.started":"2025-08-15T13:20:55.912851Z","shell.execute_reply":"2025-08-15T13:36:42.830929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lgb_params = {\n    'objective': 'lambdarank',\n    'metric': 'ndcg',\n    'eval_at': [3],\n    'seed': RANDOM_STATE,\n    'verbose': -1,\n    'learning_rate': 0.05,  # Increased from 0.03\n    'num_leaves': 31,       # Reduced from 60\n    'max_depth': 7,         # Reduced from 10\n    'min_data_in_leaf': 20,\n    'feature_fraction': 0.7,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 5,\n    'lambda_l1': 1.0,\n    'lambda_l2': 1.0\n}\n\nprint(\"\\nTraining LightGBM...\")\nlgb_model = lgb.train(\n    lgb_params,\n    lgb_train,\n    num_boost_round=1000,\n    valid_sets=[lgb_train],\n    callbacks=[lgb.early_stopping(100), lgb.log_evaluation(50)]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:36:42.838458Z","iopub.execute_input":"2025-08-15T13:36:42.838715Z","iopub.status.idle":"2025-08-15T13:44:59.463324Z","shell.execute_reply.started":"2025-08-15T13:36:42.838691Z","shell.execute_reply":"2025-08-15T13:44:59.457108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# First generate validation predictions\nxgb_va_preds = xgb_model.predict(dval)\nlgb_va_preds = lgb_model.predict(data_xgb_va.to_pandas())\n\n# Now we can test different weight combinations\nbest_score = 0\nbest_weights = (0, 0)\n\n# Test different weight combinations\nfor xgb_weight in [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8]:\n    lgb_weight = 1 - xgb_weight\n    blend_va_preds = xgb_weight * xgb_va_preds + lgb_weight * lgb_va_preds\n    current_score = hitrate_at_3(y_va, blend_va_preds, groups_va)\n    \n    if current_score > best_score:\n        best_score = current_score\n        best_weights = (xgb_weight, lgb_weight)\n\nprint(f\"Best weights: XGBoost {best_weights[0]}, LightGBM {best_weights[1]}\")\nprint(f\"Best validation score: {best_score:.5f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:44:59.465250Z","iopub.execute_input":"2025-08-15T13:44:59.465498Z","iopub.status.idle":"2025-08-15T13:45:16.982589Z","shell.execute_reply.started":"2025-08-15T13:44:59.465475Z","shell.execute_reply":"2025-08-15T13:45:16.976594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Get feature importance\nimportance = xgb_model.get_score(importance_type='gain')\nimportance = sorted(importance.items(), key=lambda x: x[1], reverse=True)\n\n# Print top 10 features\nprint(\"Top 10 features by gain:\")\nfor feat, score in importance[:10]:\n    print(f\"{feat}: {score}\")\n\n# Plot feature importance\nplt.figure(figsize=(10, 6))\nplt.bar([x[0] for x in importance[:20]], [x[1] for x in importance[:20]])\nplt.xticks(rotation=45, ha='right')\nplt.title(\"Top 20 Feature Importance (Gain)\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:45:16.984449Z","iopub.execute_input":"2025-08-15T13:45:16.984768Z","iopub.status.idle":"2025-08-15T13:45:17.420356Z","shell.execute_reply.started":"2025-08-15T13:45:16.984723Z","shell.execute_reply":"2025-08-15T13:45:17.414669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Use best weights for final predictions\nxgb_test_preds = xgb_model.predict(dtest)\nlgb_test_preds = lgb_model.predict(data_xgb_te.to_pandas())\nblend_test_preds = best_weights[0] * xgb_test_preds + best_weights[1] * lgb_test_preds\n\ndef re_rank(test: pl.DataFrame, submission_xgb: pl.DataFrame, penalty_factor=0.1):\n    COLS_TO_COMPARE = [\n        \"legs0_departureAt\",\n        \"legs0_arrivalAt\",\n        \"legs1_departureAt\",\n        \"legs1_arrivalAt\",\n        \"legs0_segments0_flightNumber\",\n        \"legs1_segments0_flightNumber\",\n        \"legs0_segments0_aircraft_code\",\n        \"legs1_segments0_aircraft_code\",\n        \"legs0_segments0_departureFrom_airport_iata\",\n        \"legs1_segments0_departureFrom_airport_iata\",\n    ]\n\n    test = test.with_columns(\n        [pl.col(c).cast(str).fill_null(\"NULL\") for c in COLS_TO_COMPARE]\n    )\n\n    df = submission_xgb.join(test, on=[\"Id\", \"ranker_id\"], how=\"left\")\n\n    df = df.with_columns(\n        (\n            pl.col(\"legs0_departureAt\")\n            + \"_\"\n            + pl.col(\"legs0_arrivalAt\")\n            + \"_\"\n            + pl.col(\"legs1_departureAt\")\n            + \"_\"\n            + pl.col(\"legs1_arrivalAt\")\n            + \"_\"\n            + pl.col(\"legs0_segments0_flightNumber\")\n            + \"_\"\n            + pl.col(\"legs1_segments0_flightNumber\")\n        ).alias(\"flight_hash\")\n    )\n\n    df = df.with_columns(\n        pl.max(\"pred_score\")\n        .over([\"ranker_id\", \"flight_hash\"])\n        .alias(\"max_score_same_flight\")\n    )\n\n    df = df.with_columns(\n        (\n            pl.col(\"pred_score\")\n            - penalty_factor * (pl.col(\"max_score_same_flight\") - pl.col(\"pred_score\"))\n        ).alias(\"reorder_score\")\n    )\n\n    df = df.with_columns(\n        pl.col(\"reorder_score\")\n        .rank(method=\"ordinal\", descending=True)\n        .over(\"ranker_id\")\n        .cast(pl.Int32)\n        .alias(\"new_selected\")\n    )\n\n    return df.select([\"Id\", \"ranker_id\", \"new_selected\", \"pred_score\", \"reorder_score\"])\n# --- Prepare Submission (unchanged except for pred_score) ---\nsubmission_xgb = (\n    test.select(['Id', 'ranker_id'])\n    .with_columns(pl.Series('pred_score', blend_test_preds))  # <-- Use blended scores\n    .with_columns(\n        pl.col('pred_score')\n        .rank(method='ordinal', descending=True)\n        .over('ranker_id')\n        .cast(pl.Int32)\n        .alias('selected')\n    )\n    .select(['Id', 'ranker_id', 'selected', 'pred_score'])\n)\n\n# --- Your Existing Re-Ranking Logic (unchanged) ---\ntop = re_rank(test, submission_xgb)\n\nfinal_submission = (\n    submission_xgb.join(top, on=[\"Id\", \"ranker_id\"], how=\"left\")\n    .with_columns(\n        pl.when(pl.col(\"new_selected\").is_not_null())\n        .then(pl.col(\"new_selected\"))\n        .otherwise(pl.col(\"selected\"))\n        .alias(\"selected\")\n    )\n    .select([\"Id\", \"ranker_id\", \"selected\"])\n)\n\nfinal_submission.write_csv('submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:45:17.423174Z","iopub.execute_input":"2025-08-15T13:45:17.423460Z","iopub.status.idle":"2025-08-15T13:45:50.251574Z","shell.execute_reply.started":"2025-08-15T13:45:17.423434Z","shell.execute_reply":"2025-08-15T13:45:50.247798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n# Add this to verify the file exists\nimport os\nif os.path.exists('/kaggle/working/submission.csv'):\n    print(\"Submission file created successfully at /kaggle/working/submission.csv\")\nelse:\n    print(\"Error: Submission file not found!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-15T13:45:50.255158Z","iopub.execute_input":"2025-08-15T13:45:50.255829Z","iopub.status.idle":"2025-08-15T13:45:50.267674Z","shell.execute_reply.started":"2025-08-15T13:45:50.255796Z","shell.execute_reply":"2025-08-15T13:45:50.262041Z"}},"outputs":[],"execution_count":null}]}