{"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":"!pip install lightgbm optuna","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:42:05.439844Z","iopub.execute_input":"2025-07-27T05:42:05.440064Z","iopub.status.idle":"2025-07-27T05:42:15.214069Z","shell.execute_reply.started":"2025-07-27T05:42:05.440040Z","shell.execute_reply":"2025-07-27T05:42:15.209461Z"}},"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\nimport pandas as pd\nimport lightgbm as lgb\nimport optuna\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.metrics import log_loss\nimport matplotlib.pyplot as plt\nimport warnings\nwarnings.filterwarnings('ignore')\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,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:42:15.216007Z","iopub.execute_input":"2025-07-27T05:42:15.216217Z","iopub.status.idle":"2025-07-27T05:42:18.488271Z","shell.execute_reply.started":"2025-07-27T05:42:15.216195Z","shell.execute_reply":"2025-07-27T05:42:18.482524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"RANDOM_STATE = 42\nnp.random.seed(RANDOM_STATE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-27T05:42:18.490435Z","iopub.execute_input":"2025-07-27T05:42:18.490882Z","iopub.status.idle":"2025-07-27T05:42:18.499613Z","shell.execute_reply.started":"2025-07-27T05:42:18.490852Z","shell.execute_reply":"2025-07-27T05:42:18.495341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. Data Loading and Preprocessing\ndef reduce_memory_usage(df):\n    for col in df.columns:\n        col_type = df[col].dtypes\n        if col_type == 'float64':\n            df[col] = pd.to_numeric(df[col], downcast='float')\n        elif col_type == 'int64':\n            df[col] = pd.to_numeric(df[col], downcast='integer')\n        elif col_type == 'object':\n            num_unique = df[col].nunique()\n            num_total = len(df[col])\n            if num_unique / num_total < 0.5:\n                df[col] = df[col].astype('category')\n    return df","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:42:18.501927Z","iopub.execute_input":"2025-07-27T05:42:18.502179Z","iopub.status.idle":"2025-07-27T05:42:18.516841Z","shell.execute_reply.started":"2025-07-27T05:42:18.502151Z","shell.execute_reply":"2025-07-27T05:42:18.511422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_parquet('/kaggle/input/aeroclub-recsys-2025/train.parquet')\ntest = pd.read_parquet('/kaggle/input/aeroclub-recsys-2025/test.parquet')\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:42:18.518641Z","iopub.execute_input":"2025-07-27T05:42:18.519229Z","iopub.status.idle":"2025-07-27T05:42:50.743607Z","shell.execute_reply.started":"2025-07-27T05:42:18.519205Z","shell.execute_reply":"2025-07-27T05:42:50.737808Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = reduce_memory_usage(train)\ntest = reduce_memory_usage(test)","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:42:50.746148Z","iopub.execute_input":"2025-07-27T05:42:50.746425Z","iopub.status.idle":"2025-07-27T05:45:07.579682Z","shell.execute_reply.started":"2025-07-27T05:42:50.746392Z","shell.execute_reply":"2025-07-27T05:45:07.573593Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4. Feature Engineering (Same as before)\ncat_features = [\n    'nationality', 'searchRoute', 'corporateTariffCode',\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]","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:45:07.581846Z","iopub.execute_input":"2025-07-27T05:45:07.582118Z","iopub.status.idle":"2025-07-27T05:45:07.594517Z","shell.execute_reply.started":"2025-07-27T05:45:07.582093Z","shell.execute_reply":"2025-07-27T05:45:07.588574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_features(df):\n    \"\"\"\n    Return a copy of df enriched with engineered features for flight-ranking models.\n    \"\"\"\n    \n\n    df = df.copy()\n\n    def hms_to_minutes(s: pd.Series) -> np.ndarray:\n        \"\"\"Vectorised 'HH:MM:SS' → minutes (seconds ignored).\"\"\"\n        mask = s.notna()\n        out = np.zeros(len(s), dtype=float)\n        if mask.any():\n            parts = s[mask].astype(str).str.split(':', expand=True)\n            out[mask] = (\n                pd.to_numeric(parts[0], errors=\"coerce\").fillna(0) * 60 +\n                pd.to_numeric(parts[1], errors=\"coerce\").fillna(0)\n            )\n        return out\n\n    # Duration columns\n    dur_cols = (\n        [\"legs0_duration\", \"legs1_duration\"] +\n        [f\"legs{l}_segments{s}_duration\" for l in (0, 1) for s in (0, 1)]\n    )\n    for col in dur_cols:\n        if col in df.columns:\n            df[col] = hms_to_minutes(df[col])\n\n    # Feature container\n    feat = {}\n\n    # Price-related features\n    feat[\"price_per_tax\"] = df[\"totalPrice\"] / (df[\"taxes\"] + 1)\n    feat[\"tax_rate\"] = df[\"taxes\"] / (df[\"totalPrice\"] + 1)\n    feat[\"log_price\"] = np.log1p(df[\"totalPrice\"])\n\n    # Duration features\n    df[\"total_duration\"] = df[\"legs0_duration\"].fillna(0) + df[\"legs1_duration\"].fillna(0)\n    feat[\"duration_ratio\"] = np.where(\n        df[\"legs1_duration\"].fillna(0) > 0,\n        df[\"legs0_duration\"] / (df[\"legs1_duration\"] + 1),\n        1.0,\n    )\n\n    # Segment counts\n    for leg in (0, 1):\n        seg_cols = [f\"legs{leg}_segments{i}_duration\" for i in (0, 1)]\n        feat[f\"n_segments_leg{leg}\"] = df[seg_cols].notna().sum(axis=1)\n    feat[\"total_segments\"] = feat[\"n_segments_leg0\"] + feat[\"n_segments_leg1\"]\n\n    # Trip type\n    feat[\"is_one_way\"] = df[\"legs1_duration\"].isna().astype(int)\n\n    # Rank features\n    grp = df.groupby(\"ranker_id\")\n    feat[\"price_rank\"] = grp[\"totalPrice\"].rank()\n    feat[\"price_pct_rank\"] = grp[\"totalPrice\"].rank(pct=True)\n    feat[\"duration_rank\"] = grp[\"total_duration\"].rank()\n    feat[\"is_cheapest\"] = (grp[\"totalPrice\"].transform(\"min\") == df[\"totalPrice\"]).astype(int)\n    feat[\"is_most_expensive\"] = (grp[\"totalPrice\"].transform(\"max\") == df[\"totalPrice\"]).astype(int)\n    feat[\"price_from_median\"] = grp[\"totalPrice\"].transform(\n        lambda x: (x - x.median()) / (x.std() + 1)\n    )\n\n    # Frequent-flyer features\n    ff = df[\"frequentFlyer\"].astype(str).fillna(\"\")\n    feat[\"n_ff_programs\"] = ff.str.count(\"/\") + (ff != \"\")\n    airlines = [\"SU\", \"S7\", \"U6\", \"TK\", \"DP\", \"UT\", \"EK\", \"N4\", \"5N\", \"LH\"]\n    for al in airlines:\n        feat[f\"ff_{al}\"] = ff.str.contains(rf\"\\b{al}\\b\").astype(int)\n    feat[\"ff_matches_carrier\"] = np.select(\n        [\n            (feat[f\"ff_{al}\"] == 1) &\n            (df[\"legs0_segments0_marketingCarrier_code\"] == al)\n            for al in [\"SU\", \"S7\", \"U6\", \"TK\"]\n        ],\n        [1, 1, 1, 1],\n        default=0,\n    )\n\n    # Binary flags\n    feat.update(\n        dict(\n            is_vip_freq=((df[\"isVip\"] == 1) | (feat[\"n_ff_programs\"] > 0)).astype(int),\n            has_return=(~df[\"legs1_duration\"].isna()).astype(int),\n            has_corporate_tariff=(~df[\"corporateTariffCode\"].isna()).astype(int),\n        )\n    )\n\n    # Baggage and fees\n    feat[\"baggage_total\"] = (\n        df[\"legs0_segments0_baggageAllowance_quantity\"].fillna(0) +\n        df[\"legs1_segments0_baggageAllowance_quantity\"].fillna(0)\n    )\n    feat[\"has_baggage\"] = (feat[\"baggage_total\"] > 0).astype(int)\n    feat[\"total_fees\"] = (\n        df[\"miniRules0_monetaryAmount\"].fillna(0) +\n        df[\"miniRules1_monetaryAmount\"].fillna(0)\n    )\n    feat[\"has_fees\"] = (feat[\"total_fees\"] > 0).astype(int)\n    feat[\"fee_rate\"] = feat[\"total_fees\"] / (df[\"totalPrice\"] + 1)\n\n    # Time-of-day\n    for col in (\"legs0_departureAt\", \"legs0_arrivalAt\", \"legs1_departureAt\", \"legs1_arrivalAt\"):\n        if col in df.columns:\n            dt = pd.to_datetime(df[col], errors=\"coerce\")\n            feat[f\"{col}_hour\"] = dt.dt.hour.fillna(12)\n            feat[f\"{col}_weekday\"] = dt.dt.weekday.fillna(0)\n            h = dt.dt.hour.fillna(12)\n            feat[f\"{col}_business_time\"] = (((6 <= h) & (h <= 9)) | ((17 <= h) & (h <= 20))).astype(int)\n\n    # Direct-flight flags\n    feat[\"is_direct_leg0\"] = (feat[\"n_segments_leg0\"] == 1).astype(int)\n    feat[\"is_direct_leg1\"] = (feat[\"n_segments_leg1\"] == 1).astype(int)\n    feat[\"both_direct\"] = feat[\"is_direct_leg0\"] & feat[\"is_direct_leg1\"]\n\n    # Cheapest direct\n    df[\"_direct\"] = feat[\"n_segments_leg0\"] == 1\n    direct_min_price = df.loc[df[\"_direct\"]].groupby(\"ranker_id\")[\"totalPrice\"].min()\n    feat[\"is_direct_cheapest\"] = (\n        df[\"_direct\"] & (df[\"totalPrice\"] == df[\"ranker_id\"].map(direct_min_price))\n    ).astype(int)\n    df.drop(columns=\"_direct\", inplace=True)\n\n    # Misc flags\n    feat[\"has_access_tp\"] = (df[\"pricingInfo_isAccessTP\"] == 1).astype(int)\n    feat[\"group_size\"] = df.groupby(\"ranker_id\")[\"Id\"].transform(\"count\")\n    feat[\"group_size_log\"] = np.log1p(feat[\"group_size\"])\n    feat[\"is_major_carrier\"] = df[\"legs0_segments0_marketingCarrier_code\"].isin([\"SU\", \"S7\", \"U6\"]).astype(int)\n    popular_routes = {\"MOWLED/LEDMOW\", \"LEDMOW/MOWLED\", \"MOWLED\", \"LEDMOW\", \"MOWAER/AERMOW\"}\n    feat[\"is_popular_route\"] = df[\"searchRoute\"].isin(popular_routes).astype(int)\n    feat[\"avg_cabin_class\"] = df[[\"legs0_segments0_cabinClass\", \"legs1_segments0_cabinClass\"]].mean(axis=1)\n    feat[\"cabin_class_diff\"] = (\n        df[\"legs0_segments0_cabinClass\"].fillna(0) - df[\"legs1_segments0_cabinClass\"].fillna(0)\n    )\n\n    # New Features\n    df[\"price_per_minute\"] = df[\"totalPrice\"] / (df[\"total_duration\"] + 1)\n\n    # Time-based features\n    for leg in [0, 1]:\n        dep_col = f\"legs{leg}_departureAt\"\n        arr_col = f\"legs{leg}_arrivalAt\"\n        if dep_col in df.columns and arr_col in df.columns:\n            dep_dt = pd.to_datetime(df[dep_col])\n            arr_dt = pd.to_datetime(df[arr_col])\n            df[f\"leg{leg}_overnight\"] = (dep_dt.dt.day != arr_dt.dt.day).astype(int)\n            df[f\"leg{leg}_departure_daypart\"] = dep_dt.dt.hour // 6\n\n    # Carrier dominance features\n    carriers = [\"SU\", \"S7\", \"U6\", \"TK\"]\n    for carrier in carriers:\n        df[f\"is_{carrier}_dominant\"] = (\n            (df[\"legs0_segments0_marketingCarrier_code\"] == carrier) &\n            (df[\"legs1_segments0_marketingCarrier_code\"] == carrier)\n        ).astype(int)\n\n    # Advanced baggage features\n    df[\"baggage_quantity_diff\"] = (\n        df[\"legs0_segments0_baggageAllowance_quantity\"] -\n        df[\"legs1_segments0_baggageAllowance_quantity\"]\n    )\n\n    # Connection quality metrics\n    for leg in [0, 1]:\n        if f\"legs{leg}_segments1_duration\" in df.columns and f\"legs{leg}_segments0_duration\" in df.columns:\n            df[f\"leg{leg}_connection_ratio\"] = (\n                df[f\"legs{leg}_segments1_duration\"] /\n                (df[f\"legs{leg}_segments0_duration\"] + 1e-6)\n            )\n\n    # Advanced group statistics\n    grp = df.groupby(\"ranker_id\")\n    df[\"price_group_skewness\"] = grp[\"totalPrice\"].transform(lambda x: x.skew())\n    df[\"duration_group_kurtosis\"] = grp[\"total_duration\"].transform(lambda x: x.kurt())\n\n    # Merge new features\n    df = pd.concat([df, pd.DataFrame(feat, index=df.index)], axis=1)\n\n    # Final NaN handling\n    for col in df.select_dtypes(include=\"number\").columns:\n        df[col] = df[col].fillna(0)\n    for col in df.select_dtypes(include=\"object\").columns:\n        df[col] = df[col].fillna(\"missing\")\n\n    return df\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:45:07.595552Z","iopub.execute_input":"2025-07-27T05:45:07.595804Z","iopub.status.idle":"2025-07-27T05:45:07.631807Z","shell.execute_reply.started":"2025-07-27T05:45:07.595780Z","shell.execute_reply":"2025-07-27T05:45:07.625764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = create_features(train)\ntest = create_features(test)","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:45:07.634054Z","iopub.execute_input":"2025-07-27T05:45:07.634280Z","iopub.status.idle":"2025-07-27T05:57:14.552291Z","shell.execute_reply.started":"2025-07-27T05:45:07.634259Z","shell.execute_reply":"2025-07-27T05:57:14.545216Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# 5. Feature Selection (Same as before)\nexclude_cols = ['Id', 'ranker_id', 'selected', 'profileId', 'requestDate',\n               'legs0_departureAt', 'legs0_arrivalAt', 'legs1_departureAt', 'legs1_arrivalAt',\n               'miniRules0_percentage', 'miniRules1_percentage',\n               'frequentFlyer']\n\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_qu00antity', '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","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:14.554510Z","iopub.execute_input":"2025-07-27T05:57:14.554827Z","iopub.status.idle":"2025-07-27T05:57:14.565661Z","shell.execute_reply.started":"2025-07-27T05:57:14.554799Z","shell.execute_reply":"2025-07-27T05:57:14.560746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for 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","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:14.567583Z","iopub.execute_input":"2025-07-27T05:57:14.567834Z","iopub.status.idle":"2025-07-27T05:57:14.582345Z","shell.execute_reply.started":"2025-07-27T05:57:14.567810Z","shell.execute_reply":"2025-07-27T05:57:14.576139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_cols = [col for col in train.columns if col not in exclude_cols]\ncat_features_final = [col for col in cat_features if col in feature_cols]\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:14.585020Z","iopub.execute_input":"2025-07-27T05:57:14.585297Z","iopub.status.idle":"2025-07-27T05:57:14.595397Z","shell.execute_reply.started":"2025-07-27T05:57:14.585272Z","shell.execute_reply":"2025-07-27T05:57:14.590119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert categorical features for LightGBM\nfor col in cat_features_final:\n    train[col] = train[col].astype('category')\n    test[col] = test[col].astype('category')","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:14.597280Z","iopub.execute_input":"2025-07-27T05:57:14.597512Z","iopub.status.idle":"2025-07-27T05:57:15.835926Z","shell.execute_reply.started":"2025-07-27T05:57:14.597491Z","shell.execute_reply":"2025-07-27T05:57:15.830791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert categorical features for LightGBM\nfor col in cat_features_final:\n    train[col] = train[col].astype('category')\n    test[col] = test[col].astype('category')\n\n# 6. Train/Validation Split\nX_train = train[feature_cols]\ny_train = train['selected']\ngroups_train = train['ranker_id']\n\nX_test = test[feature_cols]\ngroups_test = test['ranker_id']\n\ngss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=RANDOM_STATE)\ntrain_idx, val_idx = next(gss.split(X_train, y_train, groups_train))\n\nX_tr, X_val = X_train.iloc[train_idx], X_train.iloc[val_idx]\ny_tr, y_val = y_train.iloc[train_idx], y_train.iloc[val_idx]\ngroups_tr, groups_val = groups_train.iloc[train_idx], groups_train.iloc[val_idx]","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:15.838638Z","iopub.execute_input":"2025-07-27T05:57:15.838915Z","iopub.status.idle":"2025-07-27T05:57:53.152876Z","shell.execute_reply.started":"2025-07-27T05:57:15.838890Z","shell.execute_reply":"2025-07-27T05:57:53.146906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Prepare group counts for LightGBM\ntrain_groups = groups_tr.value_counts().sort_index().values\nval_groups = groups_val.value_counts().sort_index().values\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:53.155623Z","iopub.execute_input":"2025-07-27T05:57:53.155887Z","iopub.status.idle":"2025-07-27T05:57:53.291541Z","shell.execute_reply.started":"2025-07-27T05:57:53.155863Z","shell.execute_reply":"2025-07-27T05:57:53.286782Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 7. Evaluation Metrics\ndef calculate_hitrate_at_k(df, k=3):\n    hits = []\n    for ranker_id, group in df.groupby('ranker_id'):\n        if len(group) > 10:\n            top_k = group.nlargest(k, 'pred')\n            hit = (top_k['selected'] == 1).any()\n            hits.append(hit)\n    return np.mean(hits) if hits else 0.0\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T05:57:53.293567Z","iopub.execute_input":"2025-07-27T05:57:53.293833Z","iopub.status.idle":"2025-07-27T05:57:53.304817Z","shell.execute_reply.started":"2025-07-27T05:57:53.293810Z","shell.execute_reply":"2025-07-27T05:57:53.300337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 8. LightGBM Model Training\ndef train_lightgbm(params, X_tr, y_tr, X_val, y_val, train_groups, val_groups):\n    train_data = lgb.Dataset(\n        X_tr, \n        label=y_tr,\n        group=train_groups,\n        categorical_feature=cat_features_final\n    )\n    \n    val_data = lgb.Dataset(\n        X_val, \n        label=y_val,\n        group=val_groups,\n        categorical_feature=cat_features_final,\n        reference=train_data\n    )\n    \n    model = lgb.train(\n        params,\n        train_data,\n        num_boost_round=800,\n        valid_sets=[val_data],\n        callbacks=[\n            lgb.early_stopping(stopping_rounds=100),\n            lgb.log_evaluation(period=100)\n        ]\n    )\n    return model\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-27T05:57:53.306936Z","iopub.execute_input":"2025-07-27T05:57:53.307184Z","iopub.status.idle":"2025-07-27T05:57:53.318302Z","shell.execute_reply.started":"2025-07-27T05:57:53.307154Z","shell.execute_reply":"2025-07-27T05:57:53.313650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 9. Hyperparameter Tuning with Optuna\ndef objective(trial):\n    params = {\n        'objective': 'lambdarank',\n        'metric': 'ndcg',\n        'ndcg_eval_at': [3],\n        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.2,log=True),\n        'num_leaves': trial.suggest_int('num_leaves', 20, 80),\n        'max_depth': trial.suggest_int('max_depth', 3, 10),\n        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 10, 50),\n        'lambda_l1': trial.suggest_float('lambda_l1', 0, 5),\n        'lambda_l2': trial.suggest_float('lambda_l2', 0, 5),\n        'feature_fraction': trial.suggest_float('feature_fraction', 0.5, 1.0),\n        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.5, 1.0),\n        'bagging_freq': trial.suggest_int('bagging_freq', 1, 10),\n        'verbosity': -1,\n        'seed': RANDOM_STATE\n    }\n    \n    model = train_lightgbm(params, X_tr, y_tr, X_val, y_val, train_groups, val_groups)\n    \n    val_preds = model.predict(X_val)\n    val_df = pd.DataFrame({'ranker_id': groups_val, 'pred': val_preds, 'selected': y_val})\n    return calculate_hitrate_at_k(val_df, k=3)\n\nprint(\"\\nStarting hyperparameter optimization...\")\nstudy = optuna.create_study(direction='maximize')\nstudy.optimize(objective, n_trials=25,timeout= 3500)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-27T05:57:53.320866Z","iopub.execute_input":"2025-07-27T05:57:53.321076Z","iopub.status.idle":"2025-07-27T06:57:13.685498Z","shell.execute_reply.started":"2025-07-27T05:57:53.321056Z","shell.execute_reply":"2025-07-27T06:57:13.681113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 10. Train Final Model with Best Parameters\nprint(\"\\nTraining final model with best parameters...\")\nbest_params = study.best_params\nbest_params.update({\n    'objective': 'lambdarank',\n    'metric': 'ndcg',\n    'ndcg_eval_at': [3],\n    'verbosity': 50,\n    'seed': RANDOM_STATE,\n    'max_position': 10 \n})\n\nfinal_model = train_lightgbm(best_params, X_tr, y_tr, X_val, y_val, train_groups, val_groups)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-27T06:57:13.687301Z","iopub.execute_input":"2025-07-27T06:57:13.687504Z","iopub.status.idle":"2025-07-27T07:06:08.004822Z","shell.execute_reply.started":"2025-07-27T06:57:13.687482Z","shell.execute_reply":"2025-07-27T07:06:07.999621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 11. Evaluation\nval_preds = final_model.predict(X_val)\nval_df = pd.DataFrame({\n    'ranker_id': groups_val,\n    'pred': val_preds,\n    'selected': y_val\n})\n\ndef sigmoid(x):\n    return 1 / (1 + np.exp(-x / 10))\n\n# Fix: groupby with observed=True\ntop_preds = val_df.loc[val_df.groupby('ranker_id', observed=True)['pred'].idxmax()]\ntop_preds['prob'] = sigmoid(top_preds['pred'])\nval_logloss = log_loss(top_preds['selected'], top_preds['prob'])\n\nhitrate_at_3 = calculate_hitrate_at_k(val_df, k=3)\nval_accuracy = (top_preds['selected'] == 1).mean()\ngroup_sizes = val_df.groupby('ranker_id', observed=True).size()\n\nprint(\"\\nFinal Model Performance:\")\nprint(f\"HitRate@3: {hitrate_at_3:.4f}\")\nprint(f\"LogLoss: {val_logloss:.4f}\")\nprint(f\"Top-1 Accuracy: {val_accuracy:.4f}\")\nprint(f\"Avg Group Size: {group_sizes.mean():.1f}\")\n","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T07:06:08.006328Z","iopub.execute_input":"2025-07-27T07:06:08.006613Z","iopub.status.idle":"2025-07-27T07:06:45.273576Z","shell.execute_reply.started":"2025-07-27T07:06:08.006586Z","shell.execute_reply":"2025-07-27T07:06:45.267325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#12 Feature importance visualization\nlgb.plot_importance(final_model, max_num_features=20, figsize=(10, 6))\nplt.title('Feature Importance (rank_xendcg)')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-27T07:06:45.275530Z","iopub.execute_input":"2025-07-27T07:06:45.275806Z","iopub.status.idle":"2025-07-27T07:06:45.682173Z","shell.execute_reply.started":"2025-07-27T07:06:45.275779Z","shell.execute_reply":"2025-07-27T07:06:45.675832Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 13. Create Submission\ntest_preds = final_model.predict(X_test)\n\nsubmission = test[['Id', 'ranker_id']].copy()\nsubmission['pred_score'] = test_preds\nsubmission['selected'] = submission.groupby('ranker_id')['pred_score'].rank(\n    ascending=False, method='first'\n).astype(int)\n\nsubmission[['Id', 'ranker_id', 'selected']].to_csv('submission.csv', index=False)\nprint(f\"\\nSubmission saved. Shape: {submission.shape}\")","metadata":{"trusted":true,"editable":false,"execution":{"iopub.status.busy":"2025-07-27T07:06:45.685398Z","iopub.execute_input":"2025-07-27T07:06:45.685636Z","iopub.status.idle":"2025-07-27T07:07:28.088594Z","shell.execute_reply.started":"2025-07-27T07:06:45.685612Z","shell.execute_reply":"2025-07-27T07:07:28.082696Z"}},"outputs":[],"execution_count":null}]}