{"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":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"![](https://kaggle.com/competitions/105399/images/header)","metadata":{}},{"cell_type":"markdown","source":"# **Thanks for watching, if it helped or you liked it, vote so that others can see it, thanks**","metadata":{}},{"cell_type":"code","source":"# %%capture\n# !pip install -U xgboost\n# !pip install -U lightgbm\n# !pip install -U polars","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:40:58.716307Z","iopub.execute_input":"2025-08-13T21:40:58.716683Z","iopub.status.idle":"2025-08-13T21:41:20.307993Z","shell.execute_reply.started":"2025-08-13T21:40:58.716654Z","shell.execute_reply":"2025-08-13T21:41:20.304364Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Base\nimport os\nimport subprocess\nimport matplotlib.pyplot as plt\nimport lightgbm as lgb\nimport xgboost as xgb\nimport numpy as np\nimport polars as pl\nfrom sklearn.model_selection import GroupKFold\nfrom itertools import product\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom itertools import chain\n\n# Plots\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:41:20.311104Z","iopub.execute_input":"2025-08-13T21:41:20.311327Z","iopub.status.idle":"2025-08-13T21:41:25.046671Z","shell.execute_reply.started":"2025-08-13T21:41:20.311304Z","shell.execute_reply":"2025-08-13T21:41:25.040090Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Analysis","metadata":{}},{"cell_type":"markdown","source":"**Some of the Columns:**\n\n* Id - Unique identifier for each flight option\n\n* ranker_id - Group identifier for each search session (key grouping variable for ranking)\n\n* profileId - User identifier\n\n* companyID - Company identifier\n\n* sex - User gender\n\n* nationality - User nationality/citizenship\n\n* frequentFlyer - Frequent flyer program status\n\n* isVip - VIP status indicator\n\n* bySelf - Whether user books flights independently\n\n* isAccess3D - Binary marker for internal feature\n\n* Flight Timing and Duration\n\n* legs0_departureAt - Departure time for outbound flight\n\n* legs0_arrivalAt - Arrival time for outbound flight\n\n* legs0_duration - Duration of outbound flight\n\n* legs1_departureAt - Departure time for return flight\n\n* legs1_arrivalAt - Arrival time for return flight\n\n* legs1_duration - Duration of return flight","metadata":{}},{"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\nprint(\"Train shape:\", train.shape)\nprint(\"Test shape:\", test.shape)\n\n# Merge data\ntrain = train.with_columns(pl.lit(1).alias(\"is_train\"))\ntest = test.with_columns([\n    pl.lit(0).alias(\"is_train\"),\n    pl.lit(None).cast(pl.Int64).alias(\"selected\")\n])\n\ncombined = pl.concat([train, test], how=\"diagonal\")\nprint(\"Combined shape:\", combined.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:41:25.049198Z","iopub.execute_input":"2025-08-13T21:41:25.049627Z","iopub.status.idle":"2025-08-13T21:41:29.496827Z","shell.execute_reply.started":"2025-08-13T21:41:25.049585Z","shell.execute_reply":"2025-08-13T21:41:29.492237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"selected_analysis = combined.filter(pl.col(\"is_train\") == 1).group_by(\"ranker_id\").agg([\n    pl.sum(\"selected\").alias(\"selected_count\"),\n    pl.len().alias(\"total_options\")\n]).select([\n    pl.mean(\"total_options\").alias(\"avg_options_per_session\"),\n    pl.min(\"total_options\").alias(\"min_options\"),\n    pl.max(\"total_options\").alias(\"max_options\"),\n    pl.len().alias(\"total_sessions\")\n])\n\nprint(\"Selected analysis:\")\nprint(selected_analysis)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:41:29.498385Z","iopub.execute_input":"2025-08-13T21:41:29.498602Z","iopub.status.idle":"2025-08-13T21:41:32.733836Z","shell.execute_reply.started":"2025-08-13T21:41:29.498580Z","shell.execute_reply":"2025-08-13T21:41:32.727202Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Feature engineering","metadata":{}},{"cell_type":"code","source":"def create_advanced_features(df):\n    \"\"\"Creating Features\"\"\"\n    \n    # Duration conversion\n    def dur_to_min(col_name):\n        col = pl.col(col_name)\n        # days, hours, min\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(\n            col.str.replace(r\"^\\d+\\.\", \"\")\n        ).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    # Processing duration columns\n    dur_cols = [\"legs0_duration\", \"legs1_duration\"] + [\n        f\"legs{l}_segments{s}_duration\" for l in (0, 1) for s in (0, 1, 2, 3)\n    ]\n    dur_exprs = [dur_to_min(c).alias(c) for c in dur_cols if c in df.columns]\n\n    # Apply duration transformations\n    if dur_exprs:\n        df = df.with_columns(dur_exprs)\n\n    # Pre-processing of marketing carriers\n    mc_cols = [f'legs{l}_segments{s}_marketingCarrier_code' for l in (0, 1) for s in range(4)]\n    mc_exists = [col for col in mc_cols if col in df.columns]\n\n    # Basic transformations\n    df = df.with_columns([\n        # Price indicators\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        # Signs of duration\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 number of segments\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(\"total_segments_count\"),\n        \n        # Signs of a frequency program\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 and 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        # Popular routes\n        pl.col(\"searchRoute\").is_in([\"MOWLED/LEDMOW\", \"LEDMOW/MOWLED\", \"MOWLED\", \"LEDMOW\", \"MOWAER/AERMOW\"])\n          .cast(pl.Int32).alias(\"is_popular_route\"),\n        \n        # Service class\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    # Count segments for each leg\n    seg_exprs = []\n    for 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 counting\n    df = df.with_columns(seg_exprs)\n\n    # Derived features\n    df = 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    # Additional signs\n    df = 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    # Flags of major airlines\n    if \"legs0_segments0_marketingCarrier_code\" in df.columns:\n        df = df.with_columns(\n            pl.col(\"legs0_segments0_marketingCarrier_code\").is_in([\"SU\", \"S7\", \"U6\", \"Aeroflot\"])\n              .cast(pl.Int32).alias(\"is_major_carrier\")\n        )\n    else:\n        df = df.with_columns(pl.lit(0).alias(\"is_major_carrier\"))\n\n    df = df.with_columns(pl.col(\"group_size\").log1p().alias(\"group_size_log\"))\n\n    # Temporary signs\n    time_exprs = []\n    time_cols = [\"legs0_departureAt\", \"legs0_arrivalAt\", \"legs1_departureAt\", \"legs1_arrivalAt\"]\n    \n    for col_name in time_cols:\n        if col_name in df.columns:\n            # Convert the string to datetime\n            dt_col = pl.col(col_name).str.strptime(pl.Datetime, \"%Y-%m-%dT%H:%M:%S%.f\", strict=False)\n            h = dt_col.dt.hour().fill_null(12)\n            time_exprs.extend([\n                h.alias(f\"{col_name}_hour\"),\n                dt_col.dt.weekday().fill_null(0).alias(f\"{col_name}_weekday\"),\n                (((h >= 6) & (h <= 9)) | ((h >= 17) & (h <= 20))).cast(pl.Int32).alias(f\"{col_name}_business_time\"),\n                # Business hours (9-17)\n                ((h >= 9) & (h <= 17)).cast(pl.Int32).alias(f\"{col_name}_business_hours\"),\n                # Night time\n                ((h >= 22) | (h <= 6)).cast(pl.Int32).alias(f\"{col_name}_night_time\")\n            ])\n    \n    if time_exprs:\n        df = df.with_columns(time_exprs)\n\n    # Ranking features\n    df = df.with_columns([\n        pl.col(\"group_size\").log1p().alias(\"group_size_log\"),\n    ])\n\n    # Base Ranks\n    rank_exprs = []\n    for col_name, alias in [(\"totalPrice\", \"price\"), (\"total_duration\", \"duration\")]:\n        if col_name in df.columns:\n            rank_exprs.append(\n                pl.col(col_name).rank().over(\"ranker_id\").alias(f\"{alias}_rank\")\n            )\n\n    \n    price_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(\"total_segments\") == pl.col(\"total_segments\").min().over(\"ranker_id\")).cast(pl.Int32).alias(\"is_min_segments\"),\n    ]\n\n    if rank_exprs:\n        df = df.with_columns(rank_exprs + price_exprs)\n\n    # Cheapest direct flight\n    direct_cheapest = (\n        df.filter(pl.col(\"is_direct_leg0\") == 1)\n        .group_by(\"ranker_id\")\n        .agg(pl.col(\"totalPrice\").min().alias(\"min_direct_price\"))\n    )\n\n    df = 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_price\"))).cast(pl.Int32).fill_null(0).alias(\"is_direct_cheapest\")\n    ]).drop(\"min_direct_price\")\n    \n\n    return df\n\ncombined_features = create_advanced_features(combined)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:41:32.735527Z","iopub.execute_input":"2025-08-13T21:41:32.735828Z","iopub.status.idle":"2025-08-13T21:42:02.509059Z","shell.execute_reply.started":"2025-08-13T21:41:32.735798Z","shell.execute_reply":"2025-08-13T21:42:02.501302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def add_business_logic_features(df):\n    \"\"\"Adding business logic to features\"\"\"\n    \n    # First we create a temporary column for the class categories\n    df = df.with_columns([\n        pl.when(pl.col(\"legs0_segments0_cabinClass\") == 1.0).then(pl.lit(\"economy\"))\n         .when(pl.col(\"legs0_segments0_cabinClass\") == 2.0).then(pl.lit(\"business\"))\n         .when(pl.col(\"legs0_segments0_cabinClass\") == 4.0).then(pl.lit(\"premium\"))\n         .otherwise(pl.lit(\"other\")).alias(\"cabin_class_temp\")\n    ])\n    \n    # Check which columns are datetime\n    datetime_cols = []\n    string_cols = []\n    \n    for col in [\"legs0_departureAt\", \"requestDate\"]:\n        if col in df.columns:\n            if df[col].dtype == pl.Datetime:\n                datetime_cols.append(col)\n            else:\n                string_cols.append(col)\n    \n    print(f\"Datetime columns: {datetime_cols}\")\n    print(f\"String columns: {string_cols}\")\n    \n    datetime_exprs = []\n    \n    # For string columns - parse\n    for col in string_cols:\n        datetime_exprs.append(\n            pl.col(col).str.strptime(pl.Datetime, \"%Y-%m-%dT%H:%M:%S%.f\", strict=False)\n            .alias(f\"{col}_dt\")\n        )\n    \n    if datetime_exprs:\n        df = df.with_columns(datetime_exprs)\n    \n    # Determine which columns to use for calculations\n    departure_col = \"legs0_departureAt\" if \"legs0_departureAt\" in datetime_cols else \"legs0_departureAt_dt\"\n    request_col = \"requestDate\" if \"requestDate\" in datetime_cols else \"requestDate_dt\"\n    \n    df = df.with_columns([\n        # Time before departure (in days) - only if both columns exist\n        pl.when(pl.col(departure_col).is_not_null() & pl.col(request_col).is_not_null())\n          .then((pl.col(departure_col) - pl.col(request_col)).dt.total_days())\n          .otherwise(None)\n          .alias(\"days_to_departure\"),\n        \n        ((pl.col(\"legs0_departureAt_hour\") >= 6) & (pl.col(\"legs0_departureAt_hour\") <= 9)).cast(pl.Int32)\n         .alias(\"departure_early_morning\"),\n        \n        ((pl.col(\"legs0_departureAt_hour\") >= 17) & (pl.col(\"legs0_departureAt_hour\") <= 21)).cast(pl.Int32)\n         .alias(\"departure_evening\"),\n        \n        pl.col(departure_col).dt.month().alias(\"departure_month\"),\n        \n        (pl.col(\"totalPrice\") / (pl.col(\"total_duration\") / 60 + 1)).alias(\"price_per_flying_hour\"),\n        \n        (pl.col(\"legs0_segments0_seatsAvailable\").fill_null(0) + \n         pl.col(\"legs1_segments0_seatsAvailable\").fill_null(0)).alias(\"total_seats_available\"),\n        \n        (pl.col(\"legs0_segments0_baggageAllowance_quantity\").fill_null(0) >= 20).cast(pl.Int32)\n         .alias(\"has_good_baggage\"),\n        \n        (pl.col(\"legs0_segments0_baggageAllowance_quantity\").fill_null(0) > 0).cast(pl.Int32)\n         .alias(\"has_any_baggage\"),\n        \n        pl.col(\"cabin_class_temp\").alias(\"cabin_class_category\"),\n        \n    ]).drop(\"cabin_class_temp\") \n    \n    temp_dt_cols = [col for col in df.columns if col.endswith(\"_dt\")]\n    if temp_dt_cols:\n        df = df.drop(temp_dt_cols)\n    \n    return df\n\ncombined_features = add_business_logic_features(combined_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:42:02.511458Z","iopub.execute_input":"2025-08-13T21:42:02.511736Z","iopub.status.idle":"2025-08-13T21:42:04.846056Z","shell.execute_reply.started":"2025-08-13T21:42:02.511707Z","shell.execute_reply":"2025-08-13T21:42:04.842178Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modeling","metadata":{}},{"cell_type":"code","source":"# Split back into train/test\ntrain_data = combined_features.filter(pl.col(\"is_train\") == 1)\ntest_data = combined_features.filter(pl.col(\"is_train\") == 0)\n\nprint(f\"Train shape: {train_data.shape}\")\nprint(f\"Test shape: {test_data.shape}\")\n\n# Let's look at the balance of classes\nclass_balance = train_data.select([\n    pl.sum(\"selected\").alias(\"positive_samples\"),\n    pl.len().alias(\"total_samples\")\n]).with_columns([\n    (pl.col(\"positive_samples\") / pl.col(\"total_samples\") * 100).alias(\"positive_rate_percent\")\n])\n\nprint(\"Class balance:\")\nprint(class_balance)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:42:04.849683Z","iopub.execute_input":"2025-08-13T21:42:04.849910Z","iopub.status.idle":"2025-08-13T21:42:04.906354Z","shell.execute_reply.started":"2025-08-13T21:42:04.849888Z","shell.execute_reply":"2025-08-13T21:42:04.900014Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Let's define numerical features (excluding identifiers and categorical ones)\nexclude_cols = [\n    \"Id\", \"ranker_id\", \"profileId\", \"companyID\", \"searchRoute\", \n    \"legs0_departureAt\", \"legs0_arrivalAt\", \"legs1_departureAt\", \"legs1_arrivalAt\",\n    \"requestDate\", \"corporateTariffCode\", \"frequentFlyer\", \"nationality\",\n    \"legs0_segments0_marketingCarrier_code\", \"legs1_segments0_marketingCarrier_code\",\n    \"sex\", \"selected\", \"is_train\", \"cabin_class_category\"\n]\n\n# Add columns that can contain rows\nstring_cols = [col for col in train_data.columns if train_data[col].dtype == pl.Utf8]\nexclude_cols.extend([col for col in string_cols if col not in exclude_cols])\n\nfeature_cols = [col for col in train_data.columns if col not in exclude_cols and col != \"selected\"]\nprint(f\"Count features: {len(feature_cols)}\")\nprint(\"Features:\", feature_cols[:20])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:42:04.908399Z","iopub.execute_input":"2025-08-13T21:42:04.908645Z","iopub.status.idle":"2025-08-13T21:42:04.919566Z","shell.execute_reply.started":"2025-08-13T21:42:04.908621Z","shell.execute_reply":"2025-08-13T21:42:04.914839Z"}},"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    )\n\n\nprint(\"Preparing data for XGBoost...\")\n\n\nX_train = train_data.select(feature_cols)\ny_train_array = train_data.select(\"selected\").to_numpy().ravel()\ngroups_train = train_data.select(\"ranker_id\")\n\n# Calculate the sizes of groups\ngroup_sizes_train = (groups_train\n                    .group_by('ranker_id', maintain_order=True)\n                    .agg(pl.len())['len']\n                    .to_numpy())\n\nprint(f\"Train shape: {X_train.shape}\")\nprint(f\"Group sizes length: {len(group_sizes_train)}\")\n\n\ndtrain = xgb.DMatrix(\n    X_train.to_numpy(), \n    label=y_train_array, \n    group=group_sizes_train\n)\n\nxgb_params = {\n    'objective': 'rank:pairwise',  \n    'eval_metric': 'ndcg@3',       \n    'max_depth': 8,                \n    'min_child_weight': 20,       \n    'subsample': 0.8,             \n    'colsample_bytree': 0.8,       \n    'reg_lambda': 10.0,            \n    'learning_rate': 0.05,         \n    'random_state': 42,\n    'n_jobs': -1                  \n}\n\n\nprint(\"Training XGBoost model...\")\nxgb_model = xgb.train(\n    xgb_params,\n    dtrain,\n    num_boost_round=1000,\n    verbose_eval=100\n)\n\nprint(\"Model complite!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:42:05.203185Z","iopub.execute_input":"2025-08-13T21:42:05.203436Z","iopub.status.idle":"2025-08-13T21:53:13.596117Z","shell.execute_reply.started":"2025-08-13T21:42:05.203410Z","shell.execute_reply":"2025-08-13T21:53:13.591635Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Predictions on train\nprint(\"Predictions on train...\")\ntrain_predictions_xgb = xgb_model.predict(dtrain)\n\n# Metrics\nhitrate_xgb = hitrate_at_3(y_train_array, train_predictions_xgb, groups_train.to_numpy().ravel())\nprint(f\"XGBoost HitRate@3 on train: {hitrate_xgb:.4f}\")\n\n# Predictions on test\nprint(\"Predictions on test...\")\nX_test = test_data.select(feature_cols)\ndtest = xgb.DMatrix(X_test.to_numpy())\ntest_predictions_xgb = xgb_model.predict(dtest)\n\n\ndef create_submission_xgb(test_df, predictions):\n    \"\"\"Create sub\"\"\"\n    \n    test_with_pred = test_df.with_columns([\n        pl.Series(\"prediction_score\", predictions)\n    ])\n    \n    result_frames = []\n    \n    for ranker_data in test_with_pred.partition_by(\"ranker_id\"):\n        ranked_group = (ranker_data\n                       .sort(\"prediction_score\", descending=True)\n                       .with_row_index(\"rank\", offset=1)\n                       .select([\n                           \"Id\", \n                           \"ranker_id\", \n                           pl.col(\"rank\").alias(\"selected\")\n                       ]))\n        result_frames.append(ranked_group)\n    \n    submission = pl.concat(result_frames).sort(\"Id\")\n    \n    return submission\n\nfinal_submission = create_submission_xgb(test_data, test_predictions_xgb)\nprint(f\"Submission done: {final_submission.shape}\")\n\nfinal_submission.write_csv(\"submission_xgb.csv\")\nprint(\"Submission save in submission_xgb.csv!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:53:13.614872Z","iopub.execute_input":"2025-08-13T21:53:13.615102Z","iopub.status.idle":"2025-08-13T21:55:28.100610Z","shell.execute_reply.started":"2025-08-13T21:53:13.615080Z","shell.execute_reply":"2025-08-13T21:55:28.095596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"importance_dict = xgb_model_final.get_score(importance_type='gain')\nimportance_df = pl.DataFrame([\n    {'feature': k, 'importance': v} \n    for k, v in importance_dict.items()\n]).sort('importance', descending=True)\n\nprint(\"Топ-20 importance features:\")\nprint(importance_df.head(20))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-13T21:55:28.103333Z","iopub.execute_input":"2025-08-13T21:55:28.103573Z","iopub.status.idle":"2025-08-13T21:55:28.125176Z","shell.execute_reply.started":"2025-08-13T21:55:28.103551Z","shell.execute_reply":"2025-08-13T21:55:28.119483Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Thanks for watching, if it helped or you liked it, vote so that others can see it, thanks**","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}