{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":105399,"databundleVersionId":12733338,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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)\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-07-22T12:10:52.079289Z","iopub.execute_input":"2025-07-22T12:10:52.079965Z","iopub.status.idle":"2025-07-22T12:10:52.088783Z","shell.execute_reply.started":"2025-07-22T12:10:52.079934Z","shell.execute_reply":"2025-07-22T12:10:52.087875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nfrom pathlib import Path\nfrom sklearn.preprocessing import LabelEncoder\nimport gc\n\nfrom tqdm.notebook import tqdm\ntqdm.pandas()  # Enables progress bars on pandas operations\n\n# --- Configuration ---\n# Define the base directory where the competition data is located.\n# This path typically points to the input directory in a Kaggle environment.\nDATA_DIR = Path(\"/kaggle/input/aeroclub-recsys-2025/\")\n\n# Define the full paths to the training, testing, and sample submission files.\nTRAIN_PATH = DATA_DIR / \"train.parquet\"\nTEST_PATH = DATA_DIR / \"test.parquet\"\nSAMPLE_SUB_PATH = DATA_DIR / \"sample_submission.parquet\"\n\n# Define the output path for the final submission file.\n# In Kaggle, \"/kaggle/working/\" is the designated directory for output files.\nOUTPUT_PATH = Path(\"/kaggle/working/submission.parquet\")\n\n# --- Memory Optimization Function ---\n# This function is designed to reduce the memory footprint of a Pandas DataFrame.\n# Large datasets can quickly consume available RAM, leading to crashes or slow processing.\n# By downcasting numerical columns to the smallest possible data types (e.g., int64 to int8),\n# we can significantly optimize memory usage without losing data integrity.\ndef reduce_mem_usage(df, verbose=True):\n    numerics = [\"int16\", \"int32\", \"int64\", \"float16\", \"float32\", \"float64\"]\n    start_mem = df.memory_usage().sum() / 1024**2 # Calculate initial memory usage in MB\n    for col in tqdm(df.columns, desc=\"Reducing Memory\"):\n        col_type = df[col].dtypes\n        if col_type in numerics:\n            c_min = df[col].min()\n            c_max = df[col].max()\n            if str(col_type)[:3] == \"int\":\n                # Check if integer column can be downcasted to a smaller integer type\n                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:\n                    df[col] = df[col].astype(np.int8)\n                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:\n                    df[col] = df[col].astype(np.int16)\n                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:\n                    df[col] = df[col].astype(np.int32)\n                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:\n                    df[col] = df[col].astype(np.int64)\n            else:\n                # Check if float column can be downcasted to a smaller float type\n                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:\n                    df[col] = df[col].astype(np.float16)\n                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:\n                    df[col] = df[col].astype(np.float32)\n                else:\n                    df[col] = df[col].astype(np.float64)\n    end_mem = df.memory_usage().sum() / 1024**2 # Calculate final memory usage in MB\n    if verbose: \n        print(f\"Mem. usage decreased to {end_mem:5.2f} Mb ({100 * (start_mem - end_mem) / start_mem:.1f}% reduction)\")\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:10:52.113319Z","iopub.execute_input":"2025-07-22T12:10:52.113699Z","iopub.status.idle":"2025-07-22T12:10:56.808929Z","shell.execute_reply.started":"2025-07-22T12:10:52.113672Z","shell.execute_reply":"2025-07-22T12:10:56.808303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport gc\n\n# Define selected relevant columns for training\nselected_cols_train = [\n    \"Id\", \"ranker_id\", \"profileId\", \"companyID\", \"isVip\", \"bySelf\",\n    \"requestDate\", \"searchRoute\", \"totalPrice\", \"taxes\",\n    \"legs0_departureAt\", \"legs0_arrivalAt\", \"legs0_duration\",\n    \"legs1_departureAt\", \"legs1_arrivalAt\", \"legs1_duration\",\n    \"legs0_segments0_seatsAvailable\", \"legs0_segments0_cabinClass\",\n    \"legs0_segments0_flightNumber\", \"legs1_segments0_flightNumber\",\n    \"legs0_segments0_baggageAllowance_quantity\",\n    \"legs0_segments0_baggageAllowance_weightMeasurementType\",\n    \"legs0_segments0_marketingCarrier_code\",\n    \"legs0_segments0_operatingCarrier_code\",\n    \"legs0_segments0_duration\",\n    \"legs0_segments0_departureFrom_airport_iata\",\n    \"legs0_segments0_arrivalTo_airport_city_iata\",\n    \"miniRules0_statusInfos\", \"miniRules0_monetaryAmount\", \"miniRules0_percentage\",\n    \"miniRules1_statusInfos\", \"miniRules1_monetaryAmount\", \"miniRules1_percentage\",\n    \"selected\"  # Target column for training only\n]\n\n# For test data (no target column)\nselected_cols_test = [col for col in selected_cols_train if col != \"selected\"]\n\n# Load using PyArrow\ntrain = pd.read_parquet('/kaggle/input/aeroclub-recsys-2025/train.parquet', columns=selected_cols_train, engine='pyarrow')\ntest = pd.read_parquet('/kaggle/input/aeroclub-recsys-2025/test.parquet', columns=selected_cols_test, engine='pyarrow')\n\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:10:56.810138Z","iopub.execute_input":"2025-07-22T12:10:56.810722Z","iopub.status.idle":"2025-07-22T12:11:16.850957Z","shell.execute_reply.started":"2025-07-22T12:10:56.810701Z","shell.execute_reply":"2025-07-22T12:11:16.850209Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ✅ Step 1: Datetime Features","metadata":{}},{"cell_type":"markdown","source":"We'll extract key parts like hour, day, weekday, weekend, etc. from:\n\n- requestDate\n\n- legs0_departureAt, legs0_arrivalAt\n\n- legs1_departureAt, legs1_arrivalAt\n","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\nimport gc\n\ndef add_datetime_features(df):\n    print(\"Extracting datetime features...\")\n    \n    # Convert datetime columns\n    datetime_cols = [\n        'requestDate', \n        'legs0_departureAt', 'legs0_arrivalAt',\n        'legs1_departureAt', 'legs1_arrivalAt'\n    ]\n    \n    for col in tqdm(datetime_cols):\n        df[col] = pd.to_datetime(df[col], errors='coerce')\n\n    # Request datetime features\n    df['request_hour'] = df['requestDate'].dt.hour\n    df['request_dayofweek'] = df['requestDate'].dt.dayofweek\n    df['request_weekend'] = (df['request_dayofweek'] >= 5).astype(int)\n\n    # Flight leg 0 (outbound) datetime features\n    df['leg0_depart_hour'] = df['legs0_departureAt'].dt.hour\n    df['leg0_arrive_hour'] = df['legs0_arrivalAt'].dt.hour\n    df['leg0_depart_day'] = df['legs0_departureAt'].dt.dayofweek\n    df['leg0_arrive_day'] = df['legs0_arrivalAt'].dt.dayofweek\n\n    # Flight leg 1 (inbound) datetime features\n    df['leg1_depart_hour'] = df['legs1_departureAt'].dt.hour\n    df['leg1_arrive_hour'] = df['legs1_arrivalAt'].dt.hour\n    df['leg1_depart_day'] = df['legs1_departureAt'].dt.dayofweek\n    df['leg1_arrive_day'] = df['legs1_arrivalAt'].dt.dayofweek\n\n    # Flight duration (round trip / one way indicator)\n    df['round_trip'] = df['legs1_departureAt'].notna().astype(int)\n\n    # Drop original datetime columns (optional, saves memory)\n    # df.drop(datetime_cols, axis=1, inplace=True)\n\n    gc.collect()\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.851743Z","iopub.execute_input":"2025-07-22T12:11:16.851992Z","iopub.status.idle":"2025-07-22T12:11:16.858140Z","shell.execute_reply.started":"2025-07-22T12:11:16.851968Z","shell.execute_reply":"2025-07-22T12:11:16.857555Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 2: Duration & Price-Based Features","metadata":{}},{"cell_type":"markdown","source":"\nWe’ll create these custom features:\n\n- leg0_arrival_delay = arrival - departure - actual duration\n\n- leg1_arrival_delay = arrival - departure - actual duration\n\n- total_leg_duration = sum of leg0 + leg1 durations\n\n- segments_leg0_duration_diff = legs0_duration - legs0_segments0_duration\n\n- price_per_hour = totalPrice / total_leg_duration\n\n- tax_ratio = taxes / totalPrice","metadata":{}},{"cell_type":"code","source":"def add_duration_price_features(df):\n    print(\"Extracting duration and price-based features...\")\n\n    # Convert timestamps to datetime\n    df[\"legs0_departureAt\"] = pd.to_datetime(df[\"legs0_departureAt\"], errors=\"coerce\")\n    df[\"legs0_arrivalAt\"] = pd.to_datetime(df[\"legs0_arrivalAt\"], errors=\"coerce\")\n    df[\"legs1_departureAt\"] = pd.to_datetime(df[\"legs1_departureAt\"], errors=\"coerce\")\n    df[\"legs1_arrivalAt\"] = pd.to_datetime(df[\"legs1_arrivalAt\"], errors=\"coerce\")\n\n    # Convert durations to numeric (in minutes)\n    df[\"legs0_duration\"] = pd.to_numeric(df[\"legs0_duration\"], errors=\"coerce\")\n    df[\"legs1_duration\"] = pd.to_numeric(df[\"legs1_duration\"], errors=\"coerce\")\n\n    # Create delay features\n    df['leg0_arrival_delay'] = (\n        (df['legs0_arrivalAt'] - df['legs0_departureAt']).dt.total_seconds() / 60\n    ) - df['legs0_duration']\n\n    df['leg1_arrival_delay'] = (\n        (df['legs1_arrivalAt'] - df['legs1_departureAt']).dt.total_seconds() / 60\n    ) - df['legs1_duration']\n\n    # Total trip duration\n    df['total_trip_duration'] = df['legs0_duration'] + df['legs1_duration']\n\n    # Absolute duration difference\n    df['duration_diff'] = np.abs(df['legs0_duration'] - df['legs1_duration'])\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.859710Z","iopub.execute_input":"2025-07-22T12:11:16.859917Z","iopub.status.idle":"2025-07-22T12:11:16.898743Z","shell.execute_reply.started":"2025-07-22T12:11:16.859901Z","shell.execute_reply":"2025-07-22T12:11:16.898099Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 3: Route & Segment-Based Features\nWe'll extract and generate insights from:\n\n- searchRoute\n\n- legs0_segments0_departureFrom_airport_iata\n\n- legs0_segments0_arrivalTo_airport_city_iata\n\n- legs0_segments0_marketingCarrier_code\n\n- legs0_segments0_operatingCarrier_code\n\nRoute & Segment Feature Ideas:\n- route_length → number of airports in searchRoute\n\n- is_round_trip → whether route starts & ends at same city\n\n- is_same_airline → marketingCarrier == operatingCarrier\n\n- is_domestic_leg0 → based on IATA country prefix match (approximation)\n\n- same_depart_arrive_city → same airport code for dep/arr\n\n","metadata":{}},{"cell_type":"code","source":"def add_route_segment_features(df):\n    # Route string length\n    df['route_length'] = df['searchRoute'].apply(lambda x: len(str(x).split('-')))\n\n    # Is round trip\n    df['is_round_trip'] = df['searchRoute'].apply(\n        lambda x: str(x).split('-')[0] == str(x).split('-')[-1] if pd.notna(x) else False\n    ).astype(int)\n\n    # Airline consistency\n    df['is_same_airline'] = (\n        df['legs0_segments0_marketingCarrier_code'] == df['legs0_segments0_operatingCarrier_code']\n    ).astype(int)\n\n    # Domestic leg0: crude check using first and last letter of IATA\n    df['is_domestic_leg0'] = (\n        df['legs0_segments0_departureFrom_airport_iata'].str[:1] ==\n        df['legs0_segments0_arrivalTo_airport_city_iata'].str[:1]\n    ).astype(int)\n\n    # Same airport for departure and arrival\n    df['same_depart_arrive_city'] = (\n        df['legs0_segments0_departureFrom_airport_iata'] ==\n        df['legs0_segments0_arrivalTo_airport_city_iata']\n    ).astype(int)\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.899533Z","iopub.execute_input":"2025-07-22T12:11:16.899786Z","iopub.status.idle":"2025-07-22T12:11:16.914795Z","shell.execute_reply.started":"2025-07-22T12:11:16.899763Z","shell.execute_reply":"2025-07-22T12:11:16.914212Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 4: Passenger & Profile Features\nWe'll engineer features from:\n\n- isVip\n\n- bySelf\n\n- profileId\n\n- companyID\n\nThese can help model how frequent, independent, or important a traveler is\n\nFeature Ideas\n- profile_booking_freq → how many times a profileId appears in training set\n\n- company_booking_freq → same for companyID\n\n- is_self_booking → already in bySelf (convert to int if needed)\n\n- is_vip_passenger → already in isVip (convert to int)\n\n- profile_company_combo_freq → cross frequency of (profileId, companyID)","metadata":{}},{"cell_type":"code","source":"def add_passenger_profile_features(df, full_df):\n    # Frequency of each profileId\n    profile_freq = full_df['profileId'].value_counts().to_dict()\n    df['profile_booking_freq'] = df['profileId'].map(profile_freq).fillna(0).astype(int)\n\n    # Frequency of each companyID\n    company_freq = full_df['companyID'].value_counts().to_dict()\n    df['company_booking_freq'] = df['companyID'].map(company_freq).fillna(0).astype(int)\n\n    # Frequency of (profileId, companyID) pairs\n    combo_freq = full_df.groupby(['profileId', 'companyID']).size().to_dict()\n    df['profile_company_combo_freq'] = df.set_index(['profileId', 'companyID']).index.map(combo_freq).fillna(0).astype(int)\n\n    # Convert boolean columns to int\n    df['isVip'] = df['isVip'].astype(int)\n    df['bySelf'] = df['bySelf'].astype(int)\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.915431Z","iopub.execute_input":"2025-07-22T12:11:16.915663Z","iopub.status.idle":"2025-07-22T12:11:16.929960Z","shell.execute_reply.started":"2025-07-22T12:11:16.915640Z","shell.execute_reply":"2025-07-22T12:11:16.929336Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Step 5: Price-Based & Tax Features\nWe’ll engineer numeric features from:\n\ntotalPrice\n\ntaxes\nThese can reveal patterns about how expensive or tax-heavy a booking is — which may influence selection probability.\nFeature Ideas\n- price_per_leg → totalPrice divided by number of legs\n\n- tax_ratio → taxes / totalPrice\n\n- price_minus_tax → totalPrice - taxes\n\n- is_high_tax_ratio → binary flag if tax_ratio > 0.5\n\n- log_price, log_tax → use np.log1p() to reduce skewness in modeling\n\n","metadata":{}},{"cell_type":"code","source":"def add_price_tax_features(df):\n    df['price_per_leg'] = df['totalPrice'] / (\n        df[['legs0_duration', 'legs1_duration']].notnull().sum(axis=1).clip(lower=1)\n    )\n\n    df['tax_ratio'] = (df['taxes'] / df['totalPrice']).fillna(0)\n    df['price_minus_tax'] = (df['totalPrice'] - df['taxes']).fillna(0)\n\n    df['is_high_tax_ratio'] = (df['tax_ratio'] > 0.5).astype(int)\n\n    # Log transformations to reduce skew\n    df['log_price'] = np.log1p(df['totalPrice'])\n    df['log_tax'] = np.log1p(df['taxes'])\n\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.930586Z","iopub.execute_input":"2025-07-22T12:11:16.930822Z","iopub.status.idle":"2025-07-22T12:11:16.944541Z","shell.execute_reply.started":"2025-07-22T12:11:16.930801Z","shell.execute_reply":"2025-07-22T12:11:16.944032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = reduce_mem_usage(train)\ntest = reduce_mem_usage(test)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:16.945135Z","iopub.execute_input":"2025-07-22T12:11:16.945319Z","iopub.status.idle":"2025-07-22T12:11:19.963899Z","shell.execute_reply.started":"2025-07-22T12:11:16.945306Z","shell.execute_reply":"2025-07-22T12:11:19.963254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = add_datetime_features(train)\ngc.collect()\ntrain = reduce_mem_usage(train)\n\ntrain = add_duration_price_features(train)\ngc.collect()\ntrain = reduce_mem_usage(train)\n\ntrain = add_route_segment_features(train)\ngc.collect()\ntrain = reduce_mem_usage(train)\n\ntrain = add_passenger_profile_features(train, full_df=train)\ngc.collect()\ntrain = reduce_mem_usage(train)\n\ntrain = add_price_tax_features(train)\ngc.collect()\ntrain = reduce_mem_usage(train)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:11:19.964667Z","iopub.execute_input":"2025-07-22T12:11:19.964943Z","iopub.status.idle":"2025-07-22T12:13:12.341693Z","shell.execute_reply.started":"2025-07-22T12:11:19.964918Z","shell.execute_reply":"2025-07-22T12:13:12.340817Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### What's Working Well\n- Datetime and duration features were applied successfully.\n\n- Memory reduction is working, though the benefit varies depending on column types.\n\n- we're now past the first few stages of feature engineering without a memory crash — that’s a solid win!","metadata":{}},{"cell_type":"markdown","source":"# For Test Data - apply_feature_engineering_to_test","metadata":{}},{"cell_type":"code","source":"# Recreate mini profile-company dataframe used in passenger profile features\nfull_profile_company_df = pd.concat([\n    train[[\"profileId\", \"companyID\"]],\n    test[[\"profileId\", \"companyID\"]]\n], axis=0)\nfull_profile_company_df = reduce_mem_usage(full_profile_company_df)\n\n# Apply feature engineering functions sequentially\ntest = add_datetime_features(test)\ngc.collect()\ntest = reduce_mem_usage(test)\n\ntest = add_duration_price_features(test)\ngc.collect()\ntest = reduce_mem_usage(test)\n\ntest = add_route_segment_features(test)\ngc.collect()\ntest = reduce_mem_usage(test)\n\ntest = add_passenger_profile_features(test, full_df=full_profile_company_df)\ngc.collect()\ntest = reduce_mem_usage(test)\n\ntest = add_price_tax_features(test)\ngc.collect()\ntest = reduce_mem_usage(test)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:13:12.343636Z","iopub.execute_input":"2025-07-22T12:13:12.343858Z","iopub.status.idle":"2025-07-22T12:13:57.147434Z","shell.execute_reply.started":"2025-07-22T12:13:12.343841Z","shell.execute_reply":"2025-07-22T12:13:57.146710Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:13:57.148258Z","iopub.execute_input":"2025-07-22T12:13:57.148621Z","iopub.status.idle":"2025-07-22T12:13:57.152942Z","shell.execute_reply.started":"2025-07-22T12:13:57.148595Z","shell.execute_reply":"2025-07-22T12:13:57.152161Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#  Label Encoding for Categorical Features","metadata":{}},{"cell_type":"code","source":"# Fill NaNs in object-type (categorical) columns with \"missing\"\ncat_cols = train.select_dtypes(include=[\"object\", \"category\"]).columns\n\nfor col in cat_cols:\n    train[col] = train[col].fillna(\"missing\")\n    test[col] = test[col].fillna(\"missing\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:13:57.153733Z","iopub.execute_input":"2025-07-22T12:13:57.154053Z","iopub.status.idle":"2025-07-22T12:14:20.117179Z","shell.execute_reply.started":"2025-07-22T12:13:57.154031Z","shell.execute_reply":"2025-07-22T12:14:20.116611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.preprocessing import LabelEncoder\n\n# Identify categorical columns\ncat_cols = train.select_dtypes(include='object').columns.tolist()\n\n# Encode only if column exists in both train and test\nfor col in cat_cols:\n    if col in test.columns:\n        le = LabelEncoder()\n        all_vals = pd.concat([train[col], test[col]], axis=0).astype(str)\n        le.fit(all_vals)\n        train[col] = le.transform(train[col].astype(str))\n        test[col] = le.transform(test[col].astype(str))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:14:20.117888Z","iopub.execute_input":"2025-07-22T12:14:20.118085Z","iopub.status.idle":"2025-07-22T12:15:10.171729Z","shell.execute_reply.started":"2025-07-22T12:14:20.118070Z","shell.execute_reply":"2025-07-22T12:15:10.170938Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Next Logical Step: Model Preparation and Training","metadata":{}},{"cell_type":"code","source":"import gc\nimport lightgbm as lgb\nfrom lightgbm.callback import CallbackEnv\nfrom tqdm import tqdm\nimport numpy as np\n\n# --------------------------\n# 1. Select Features\n# --------------------------\nprint(\"Preparing data for training...\")\n\nfeatures = [col for col in train.columns if col not in [\n    \"Id\", \"ranker_id\", \"selected\", \n    \"legs0_departureAt\", \"legs0_arrivalAt\", \n    \"legs1_departureAt\", \"legs1_arrivalAt\", \n    \"requestDate\", \n    \"legs0_segments1_flightNumber\", \"legs1_segments0_flightNumber\"\n]]\n\n# --------------------------\n# 2. Time-Based Split\n# --------------------------\ntrain_cutoff_date = train[\"requestDate\"].quantile(0.85, interpolation=\"nearest\")\ntrain_idx = train[train[\"requestDate\"] <= train_cutoff_date].index\nval_idx = train[train[\"requestDate\"] > train_cutoff_date].index\n\nX_train = train.loc[train_idx, features]\ny_train = train.loc[train_idx, \"selected\"]\nX_val = train.loc[val_idx, features]\ny_val = train.loc[val_idx, \"selected\"]\n\n# Group sizes for LambdaRank\ntrain_groups = train.loc[train_idx].groupby(\"ranker_id\").size().to_numpy()\nval_groups = train.loc[val_idx].groupby(\"ranker_id\").size().to_numpy()\n\n# --------------------------\n# 3. LightGBM Dataset\n# --------------------------\nlgb_train = lgb.Dataset(X_train, y_train, group=train_groups, free_raw_data=False)\nlgb_val = lgb.Dataset(X_val, y_val, group=val_groups, reference=lgb_train, free_raw_data=False)\n\n# Clean memory\ndel X_train, y_train, X_val, y_val, train\ngc.collect()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:15:10.172626Z","iopub.execute_input":"2025-07-22T12:15:10.172913Z","iopub.status.idle":"2025-07-22T12:15:42.245660Z","shell.execute_reply.started":"2025-07-22T12:15:10.172889Z","shell.execute_reply":"2025-07-22T12:15:42.244928Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --------------------------\n# 4. TQDM Callback\n# --------------------------\nclass TQDMProgressBar:\n    def __init__(self, total):\n        self.pbar = tqdm(total=total)\n\n    def __call__(self, env: CallbackEnv):\n        self.pbar.update(1)\n        if env.iteration + 1 == self.pbar.total:\n            self.pbar.close()\n\n# --------------------------\n# 5. Custom Eval: HitRate@3\n# --------------------------\ndef hit_rate_at_3(preds, train_data):\n    labels = train_data.get_label()\n    group = train_data.get_group()\n    \n    hits = 0\n    current = 0\n    for size in group:\n        group_preds = preds[current:current + size]\n        group_labels = labels[current:current + size]\n        top3_idx = np.argsort(-group_preds)[:3]\n        if any(group_labels[top3_idx]):\n            hits += 1\n        current += size\n    return 'HitRate@3', hits / len(group), True\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:15:42.246506Z","iopub.execute_input":"2025-07-22T12:15:42.246807Z","iopub.status.idle":"2025-07-22T12:15:42.252252Z","shell.execute_reply.started":"2025-07-22T12:15:42.246789Z","shell.execute_reply":"2025-07-22T12:15:42.251580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"params = {\n    \"objective\": \"lambdarank\",\n    \"boosting_type\": \"gbdt\",\n    \"metric\": \"None\",  # We will use our own\n    \"ndcg_eval_at\": [3],\n    \"verbosity\": -1,\n    \"learning_rate\": 0.05,\n    \"num_leaves\": 64,\n    \"feature_fraction\": 0.8,\n    \"bagging_fraction\": 0.8,\n    \"bagging_freq\": 5,\n    \"seed\": 42,\n    # GPU settings for P100 (adjust based on your GPU)\n    \"device_type\": \"gpu\",\n    \"gpu_platform_id\": 0,\n    \"gpu_device_id\": 0 # Can be higher on GPU for better performance\n}\n\nprint(\"Training LightGBM model...\")\nnum_boost_round = 1000\n\nmodel = lgb.train(\n    params,\n    lgb_train,\n    valid_sets=[lgb_val],\n    valid_names=[\"val\"],\n    feval=hit_rate_at_3,\n    num_boost_round=num_boost_round,\n    callbacks=[\n        TQDMProgressBar(total=num_boost_round),\n        lgb.early_stopping(stopping_rounds=50),\n        lgb.log_evaluation(period=100)\n    ]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:15:42.253014Z","iopub.execute_input":"2025-07-22T12:15:42.253193Z","iopub.status.idle":"2025-07-22T12:33:13.325558Z","shell.execute_reply.started":"2025-07-22T12:15:42.253179Z","shell.execute_reply":"2025-07-22T12:33:13.324107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# print(\"Predicting on test set...\")\n# test_preds = model.predict(test[features], num_iteration=model.best_iteration)\n\n# # Prepare dataframe for ranking\n# submission_df = test[[\"Id\", \"ranker_id\"]].copy()\n# submission_df[\"score\"] = test_preds\n\n# # Sort by ranker_id and prediction score (descending)\n# submission_df = submission_df.sort_values([\"ranker_id\", \"score\"], ascending=[True, False])\n\n# # Rank items within each group\n# submission_df[\"rank\"] = submission_df.groupby(\"ranker_id\")[\"score\"].rank(\"first\", ascending=False)\n\n# # Pick top-ranked Id per ranker_id (i.e., rank 1)\n# top_submission = submission_df.loc[submission_df[\"rank\"] == 1, [\"Id\"]]\n\n# # Save to CSV\n# top_submission.to_csv(\"submission.csv\", index=False)\n\n# print(\"submission.csv\")\nprint(\"Predicting on test set...\")\n\n# Predict scores\ntest_preds = model.predict(test[features], num_iteration=model.best_iteration)\n\n# Prepare dataframe for ranking\nsubmission_df = test[[\"Id\", \"ranker_id\"]].copy()\nsubmission_df[\"score\"] = test_preds\n\n# Sort by ranker_id and prediction score (descending)\nsubmission_df = submission_df.sort_values([\"ranker_id\", \"score\"], ascending=[True, False])\n\n# Assign rank within each group (1 is highest score)\nsubmission_df[\"selected\"] = submission_df.groupby(\"ranker_id\")[\"score\"].rank(\"first\", ascending=False).astype(int)\n\n# Save final submission\nfinal_submission = submission_df[[\"Id\", \"ranker_id\", \"selected\"]]\nfinal_submission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv created with shape:\", final_submission.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:50:20.538023Z","iopub.execute_input":"2025-07-22T12:50:20.538667Z","iopub.status.idle":"2025-07-22T12:52:46.038080Z","shell.execute_reply.started":"2025-07-22T12:50:20.538644Z","shell.execute_reply":"2025-07-22T12:52:46.037209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nprint(\"Submission head:\")\nprint(final_submission.head())\nprint(\"\\nSubmission tail:\")\nprint(final_submission.tail())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-22T12:53:26.220998Z","iopub.execute_input":"2025-07-22T12:53:26.221539Z","iopub.status.idle":"2025-07-22T12:53:26.228350Z","shell.execute_reply.started":"2025-07-22T12:53:26.221517Z","shell.execute_reply":"2025-07-22T12:53:26.227515Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}