{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":624915,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":470279,"modelId":486172}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"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\n#for 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-11-23T06:14:52.173120Z","iopub.execute_input":"2025-11-23T06:14:52.173336Z","iopub.status.idle":"2025-11-23T06:14:54.369926Z","shell.execute_reply.started":"2025-11-23T06:14:52.173315Z","shell.execute_reply":"2025-11-23T06:14:54.368870Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Imports, Device Setup, and Data Loading","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport lightgbm as lgb\nfrom sklearn.model_selection import train_test_split, KFold\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import mean_squared_error, mean_absolute_error\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\nfrom PIL import Image\nimport os\nfrom tqdm import tqdm\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# Load data\ndf_train = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\ndf_test = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:14:54.370977Z","iopub.execute_input":"2025-11-23T06:14:54.371449Z","iopub.status.idle":"2025-11-23T06:15:11.405672Z","shell.execute_reply.started":"2025-11-23T06:14:54.371416Z","shell.execute_reply":"2025-11-23T06:15:11.404633Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Preprocessing (Improved Feature Handling)","metadata":{}},{"cell_type":"code","source":"# Pivot train data to wide format\ndf_train_wide = df_train.pivot_table(\n    index='image_path', \n    columns='target_name', \n    values='target'\n).reset_index()\n\n# Define ALL required feature columns (both training and testing)\nREQUIRED_FEATURE_COLS = ['Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\n\n# --- TRAIN FEATURE MERGE ---\n# Get available features from the original train data\ntrain_feature_cols = [col for col in REQUIRED_FEATURE_COLS if col in df_train.columns]\n\n# Extract and merge features (take first occurrence for each image)\ndf_train_features = df_train.groupby('image_path')[train_feature_cols].first().reset_index()\n\n# Merge features into the wide format dataframe\ndf_train_wide = df_train_wide.merge(df_train_features, on='image_path', how='left')\n\n# --- TEST FEATURE MERGE ---\ndf_test_unique = df_test.drop_duplicates(subset=['image_path']).copy()\ntest_feature_cols = [col for col in REQUIRED_FEATURE_COLS if col in df_test.columns]\n\n# Extract and merge features\nif test_feature_cols:\n    df_test_features = df_test.groupby('image_path')[test_feature_cols].first().reset_index()\n    # Ensure all image paths are present, even if they have no features\n    df_test_unique = df_test_unique[['image_path']].merge(df_test_features, on='image_path', how='left')\nelse:\n    # If no features in test.csv, keep only image_path\n    df_test_unique = df_test[['image_path']].drop_duplicates().reset_index(drop=True)\n\n# --- ENSURE ALL FEATURE COLUMNS EXIST IN BOTH DATAFRAMES ---\n# Add missing feature columns as NaNs to prevent KeyError in Cell 3\nfor col in REQUIRED_FEATURE_COLS:\n    if col not in df_train_wide.columns:\n        df_train_wide[col] = np.nan\n    if col not in df_test_unique.columns:\n        df_test_unique[col] = np.nan\n\nprint(\"Data structures merged. All required feature columns are now present in both dataframes (potentially as NaN).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:15:11.407510Z","iopub.execute_input":"2025-11-23T06:15:11.407872Z","iopub.status.idle":"2025-11-23T06:15:11.469022Z","shell.execute_reply.started":"2025-11-23T06:15:11.407848Z","shell.execute_reply":"2025-11-23T06:15:11.467994Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Tabular Feature Engineering (Core Improvement)","metadata":{}},{"cell_type":"code","source":"tabular_features = []\n\n# --- 1. DATE FEATURE ENGINEERING ---\nfor df in [df_train_wide, df_test_unique]:\n    # Convert to datetime (use errors='coerce' to turn invalid dates into NaT)\n    df['Sampling_Date'] = pd.to_datetime(df['Sampling_Date'], errors='coerce')\n    \n    # Extract temporal features\n    df['Year'] = df['Sampling_Date'].dt.year\n    df['Month'] = df['Sampling_Date'].dt.month\n    df['Day_of_Year'] = df['Sampling_Date'].dt.dayofyear # Best feature\n\ntabular_features.extend(['Year', 'Month', 'Day_of_Year'])\n\n\n# --- 2. NUMERICAL FEATURE Imputation (Corrected) ---\nfor num_col in ['Pre_GSHH_NDVI', 'Height_Ave_cm']:\n    # Calculate median from TRAIN set only\n    train_median = df_train_wide[num_col].median()\n    \n    # Impute ONLY NaNs in both TRAIN and TEST using the TRAIN median\n    df_train_wide[num_col].fillna(train_median, inplace=True)\n    df_test_unique[num_col].fillna(train_median, inplace=True)\n    \n    tabular_features.append(num_col)\n\n\n# --- 3. CATEGORICAL FEATURE Encoding (Aligned) ---\ndef safe_label_transform(le, series, default_val=-1):\n    \"\"\"Safely transform a series using a fitted LabelEncoder, assigning default_val \n       to labels not seen during fit.\"\"\"\n    labels = set(le.classes_)\n    mapping = {label: le.transform([label])[0] for label in labels}\n    \n    encoded = series.astype(str).map(mapping).fillna(default_val).astype(int)\n    return encoded\n\n\n# Handling State\nle_state = LabelEncoder()\ndf_train_wide['State_encoded'] = le_state.fit_transform(df_train_wide['State'].astype(str))\ndf_test_unique['State_encoded'] = safe_label_transform(le_state, df_test_unique['State'], default_val=-1)\ntabular_features.append('State_encoded')\n\n# Handling Species\nle_species = LabelEncoder()\n#df_train_wide['Species_encoded'] = le_species.fit_transform(df_train_wide['Species'].astype(str))\n#df_test_unique['Species_encoded'] = safe_label_transform(le_species, df_test_unique['Species'], default_val=-1)\n#tabular_features.append('Species_encoded')\n\n# In your Tabular Feature Processing section:\ndf_train_wide['Sampling_Date'] = pd.to_datetime(df_train_wide['Sampling_Date'])\ndf_train_wide['Year'] = df_train_wide['Sampling_Date'].dt.year\ndf_train_wide['Month'] = df_train_wide['Sampling_Date'].dt.month\ndf_train_wide['Day_of_Year'] = df_train_wide['Sampling_Date'].dt.dayofyear # Best feature\n\n# Apply the same to df_test_unique\n\ntabular_features.extend(['Year', 'Month', 'Day_of_Year'])\nprint(f\"Tabular features: {tabular_features}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:15:11.470152Z","iopub.execute_input":"2025-11-23T06:15:11.470530Z","iopub.status.idle":"2025-11-23T06:15:11.505880Z","shell.execute_reply.started":"2025-11-23T06:15:11.470463Z","shell.execute_reply":"2025-11-23T06:15:11.504635Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CNN Feature Extraction (Speed-up with PyTorch DataLoader)","metadata":{}},{"cell_type":"code","source":"# --- 1. SETUP MODEL AND PREPROCESSING ---\nbase_model = models.efficientnet_b0(weights=None)\nbase_model.load_state_dict(torch.load(\n    \"/kaggle/input/efficientnet-b0-imagenet-pytorch/pytorch/default/2/efficientnet_b0_imagenet_pytorch.pth\",\n    weights_only=True\n))\n# Use nn.Sequential to remove the classifier and keep the feature extractor + pooling\n# This gives the 1280-dim feature vector after Global Average Pooling (the standard feature extractor output for B0)\nbase_model = nn.Sequential(*list(base_model.children())[:-1]) \nbase_model.eval()\nbase_model.to(device)\n\npreprocess = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n\n# --- 2. CUSTOM DATASET FOR BATCHING ---\nclass BiomassImageDataset(Dataset):\n    def __init__(self, image_paths, base_path='/kaggle/input/csiro-biomass/', transform=None):\n        self.image_paths = image_paths\n        self.base_path = base_path\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, idx):\n        img_path = self.image_paths[idx]\n        full_path = os.path.join(self.base_path, img_path)\n        \n        try:\n            img = Image.open(full_path).convert('RGB')\n            if self.transform:\n                img_tensor = self.transform(img)\n            else:\n                img_tensor = transforms.ToTensor()(img)\n        except Exception as e:\n            # Return a tensor of zeros on error\n            print(f\"Error loading {img_path}: {e}\")\n            img_tensor = torch.zeros((3, 224, 224)) # Default size for EfficientNet\n            \n        return img_tensor, img_path\n\n\n# --- 3. BATCH FEATURE EXTRACTION FUNCTION ---\ndef batch_extract_features(df, model, preprocess_fn, batch_size=64):\n    image_paths = df['image_path'].tolist()\n    dataset = BiomassImageDataset(image_paths, transform=preprocess_fn)\n    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=2)\n\n    all_features = []\n    \n    print(f\"Extracting features for {len(image_paths)} images...\")\n    with torch.no_grad():\n        for inputs, paths in tqdm(dataloader, desc=\"CNN Feature Extraction\"):\n            inputs = inputs.to(device)\n            features = model(inputs)\n            # Reshape features from (batch_size, 1280, 1, 1) to (batch_size, 1280)\n            features = features.squeeze().cpu().numpy()\n            \n            # Handle the case where batch size is 1 (squeeze behavior changes)\n            if features.ndim == 1:\n                 features = features.reshape(1, -1)\n                 \n            all_features.append(features)\n            \n    return np.concatenate(all_features, axis=0)\n\n\n# Extract CNN features for TRAIN and TEST\ntrain_cnn_features = batch_extract_features(df_train_wide, base_model, preprocess)\ntest_cnn_features = batch_extract_features(df_test_unique, base_model, preprocess)\n\nprint(f\"Train CNN features shape: {train_cnn_features.shape}\")\nprint(f\"Test CNN features shape: {test_cnn_features.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:15:11.506987Z","iopub.execute_input":"2025-11-23T06:15:11.507529Z","iopub.status.idle":"2025-11-23T06:15:47.229720Z","shell.execute_reply.started":"2025-11-23T06:15:11.507468Z","shell.execute_reply":"2025-11-23T06:15:47.228580Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Combine Features and Setup Model Training","metadata":{}},{"cell_type":"code","source":"# Combine features\nX_train_tabular = df_train_wide[tabular_features].values\nX_train_combined = np.concatenate([X_train_tabular, train_cnn_features], axis=1)\n\nX_test_tabular = df_test_unique[tabular_features].values\nX_test_combined = np.concatenate([X_test_tabular, test_cnn_features], axis=1)\n\nprint(f\"Train combined features shape: {X_train_combined.shape}\")\nprint(f\"Test combined features shape: {X_test_combined.shape}\")\n\n# Get target columns\nexclude_cols = ['image_path', 'Sampling_Date', 'State', 'Species', 'State_encoded', 'Species_encoded']\ntarget_cols = [col for col in df_train_wide.columns if col not in exclude_cols and col not in tabular_features]\nprint(f\"Target columns: {target_cols}\")\n\nprint(\"\\nTraining LightGBM models with K-Fold Cross Validation...\")\n\n# K-Fold setup\nN_FOLDS = 5\nkf = KFold(n_splits=N_FOLDS, shuffle=True, random_state=42)\n\n# Store models and predictions\nmodels = {}\noof_predictions = {}\ntest_predictions_folds = {}\nmetrics = {}\n\nlgb_params = {\n    'objective': 'regression',\n    'metric': 'rmse',\n    'boosting_type': 'gbdt',\n    'num_leaves': 64,  # Increased\n    'learning_rate': 0.05,\n    'feature_fraction': 0.8,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 5,\n    'verbose': -1,\n    'n_estimators': 1500, # Increased, relying on early stopping\n    'random_state': 42\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:15:47.231163Z","iopub.execute_input":"2025-11-23T06:15:47.231574Z","iopub.status.idle":"2025-11-23T06:15:47.247738Z","shell.execute_reply.started":"2025-11-23T06:15:47.231529Z","shell.execute_reply":"2025-11-23T06:15:47.246846Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#  Optuna Hyperparameter Tuning Cell","metadata":{}},{"cell_type":"code","source":"import optuna\nimport lightgbm as lgb\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import mean_squared_error\nimport numpy as np\nfrom tqdm.auto import tqdm\n\n# --- Configuration ---\n# DYNAMIC FIX: Get the target list and select the first one for tuning.\n# If you want to tune a different one, change the index [0].\nTARGET_TO_TUNE = target_cols[0] \nN_TRIALS = 50  # Number of optimization runs.\nN_TUNING_FOLDS = 3 # Use 3-fold for speed during tuning.\n\nprint(f\"Starting Optuna tuning for target: {TARGET_TO_TUNE} with {N_TUNING_FOLDS} folds and {N_TRIALS} trials.\")\n\n# Get the combined features and the specific target array\nX = X_train_combined\ny = df_train_wide[TARGET_TO_TUNE].values \nkf_tune = KFold(n_splits=N_TUNING_FOLDS, shuffle=True, random_state=42)\n\n\ndef objective(trial):\n    \"\"\"\n    Objective function for Optuna to minimize (RMSE).\n    \"\"\"\n    # 1. Suggest Hyperparameters\n    param = {\n        'objective': 'regression',\n        'metric': 'rmse',\n        'boosting_type': 'gbdt',\n        'n_estimators': 3000,\n        'verbose': -1,\n        'n_jobs': -1,\n        'seed': 42,\n        \n        # --- Parameters to Tune ---\n        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.05, log=True),\n        'num_leaves': trial.suggest_int('num_leaves', 31, 256),\n        'max_depth': trial.suggest_int('max_depth', 5, 15),\n        'feature_fraction': trial.suggest_float('feature_fraction', 0.6, 0.9),\n        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.6, 0.9),\n        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),\n        'min_child_samples': trial.suggest_int('min_child_samples', 20, 100),\n        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),\n        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),\n    }\n\n    oof_preds = np.zeros(len(X))\n    fold_rmses = []\n    \n    # 2. Cross-Validation Training\n    for fold, (train_idx, val_idx) in enumerate(kf_tune.split(X, y)):\n        X_tr, X_val = X[train_idx], X[val_idx]\n        y_tr, y_val = y[train_idx], y[val_idx]\n        \n        model = lgb.LGBMRegressor(**param)\n        model.fit(\n            X_tr, y_tr,\n            eval_set=[(X_val, y_val)],\n            eval_metric='rmse',\n            callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]\n        )\n        \n        preds = model.predict(X_val)\n        oof_preds[val_idx] = preds\n        fold_rmses.append(mean_squared_error(y_val, preds, squared=False))\n\n    # 3. Return the overall CV score (the metric to minimize)\n    avg_rmse = np.mean(fold_rmses)\n    return avg_rmse\n\n# Create and run the Optuna study\nstudy = optuna.create_study(direction='minimize', study_name=f\"{TARGET_TO_TUNE}_LGBM_Tuning\")\nstudy.optimize(objective, n_trials=N_TRIALS, show_progress_bar=True)\n\nprint(\"\\n\" + \"=\"*50)\nprint(f\"Optuna Study finished for {TARGET_TO_TUNE}.\")\nprint(f\"Best RMSE: {study.best_value:.4f}\")\nprint(\"Best Hyperparameters:\")\nprint(study.best_params)\nprint(\"=\"*50)\n\n# Save the best parameters to use in your main training loop\nbest_lgbm_params = study.best_params\n\n# Clean up parameter names for LGBMRegressor init\nlgb_params_tuned = {\n    'objective': 'regression',\n    'metric': 'rmse',\n    'boosting_type': 'gbdt',\n    'n_estimators': 3000,\n    'verbose': -1,\n    'n_jobs': -1,\n    'seed': 42,\n    # Map back the names\n    'lambda_l1': best_lgbm_params['lambda_l1'],\n    'lambda_l2': best_lgbm_params['lambda_l2'],\n    'feature_fraction': best_lgbm_params['feature_fraction'],\n    'bagging_fraction': best_lgbm_params['bagging_fraction'],\n    'bagging_freq': best_lgbm_params['bagging_freq'],\n    'min_child_samples': best_lgbm_params['min_child_samples'],\n    'learning_rate': best_lgbm_params['learning_rate'],\n    'num_leaves': best_lgbm_params['num_leaves'],\n    'max_depth': best_lgbm_params['max_depth'],\n}\n\nprint(\"\\n💡 New Optimized LGBM Parameters Ready for Final CV:\")\nprint(lgb_params_tuned)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:15:47.248822Z","iopub.execute_input":"2025-11-23T06:15:47.249097Z","iopub.status.idle":"2025-11-23T06:18:20.321803Z","shell.execute_reply.started":"2025-11-23T06:15:47.249071Z","shell.execute_reply":"2025-11-23T06:18:20.320684Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training Loop and Final Submission","metadata":{}},{"cell_type":"code","source":"for target_col in tqdm(target_cols, desc=\"Training models\"):\n    print(f\"\\nTraining {target_col}...\")\n    \n    # Initialize storage for this target\n    models[target_col] = []\n    oof_preds = np.zeros(len(X_train_combined))\n    test_preds = np.zeros(len(X_test_combined))\n    \n    # Get target values\n    y_train = df_train_wide[target_col].values\n    \n    # K-Fold cross validation\n    for fold, (train_idx, val_idx) in enumerate(kf.split(X_train_combined), 1):\n        X_tr, X_val = X_train_combined[train_idx], X_train_combined[val_idx]\n        y_tr, y_val = y_train[train_idx], y_train[val_idx]\n        \n        # Train LightGBM model\n        model = lgb.LGBMRegressor(**lgb_params)\n        model.fit(\n            X_tr, y_tr,\n            eval_set=[(X_val, y_val)],\n            callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]\n        )\n        \n        # Store model\n        models[target_col].append(model)\n        \n        # Out-of-fold predictions\n        oof_preds[val_idx] = model.predict(X_val)\n        \n        # Test predictions (will be averaged later)\n        test_preds += model.predict(X_test_combined) / N_FOLDS\n        \n        # Fold metrics\n        fold_rmse = np.sqrt(mean_squared_error(y_val, oof_preds[val_idx]))\n        fold_mae = mean_absolute_error(y_val, oof_preds[val_idx])\n        print(f\"  Fold {fold} - RMSE: {fold_rmse:.4f}, MAE: {fold_mae:.4f}\")\n    \n    # Store predictions\n    oof_predictions[target_col] = oof_preds\n    test_predictions_folds[target_col] = test_preds\n    \n    # Calculate overall CV metrics\n    cv_rmse = np.sqrt(mean_squared_error(y_train, oof_preds))\n    cv_mae = mean_absolute_error(y_train, oof_preds)\n    metrics[target_col] = {'CV_RMSE': cv_rmse, 'CV_MAE': cv_mae}\n    print(f\"  Overall CV - RMSE: {cv_rmse:.4f}, MAE: {cv_mae:.4f}\")\n\nmetrics_df = pd.DataFrame(metrics).T\nmetrics_df = metrics_df.sort_values('CV_RMSE')\nprint(\"\\n\" + \"=\"*60)\nprint(\"Cross-Validation Results:\")\nprint(\"=\"*60)\nprint(metrics_df.to_string())\n\nprint(f\"\\nAverage CV RMSE: {metrics_df['CV_RMSE'].mean():.4f}\")\nprint(f\"Average CV MAE: {metrics_df['CV_MAE'].mean():.4f}\")\n\n# Generate test predictions and submission (same logic)\ntest_predictions = pd.DataFrame()\ntest_predictions['image_path'] = df_test_unique['image_path']\n\nfor target_col in target_cols:\n    test_predictions[target_col] = test_predictions_folds[target_col]\n\nsubmission = test_predictions.melt(\n    id_vars=['image_path'],\n    value_vars=target_cols,\n    var_name='target_name',\n    value_name='target'\n)\n\nsubmission['image_id'] = submission['image_path'].str.extract(r'/(ID\\d+)\\.')[0]\nsubmission['sample_id'] = submission['image_id'] + '__' + submission['target_name']\nsubmission_final = submission[['sample_id', 'target']].copy()\n\n# Ensure predictions are non-negative\nsubmission_final['target'] = submission_final['target'].clip(lower=0)\n\nprint(\"\\nFinal submission dataframe generated and clipped to non-negative values.\")\n\n# Ensure predictions are non-negative\nsubmission_final['target'] = submission_final['target'].clip(lower=0)\n\n# --- Output and Saving (your request) ---\nprint(\"\\nSubmission format:\")\nprint(submission_final.head(10))\nprint(f\"\\nTotal predictions: {len(submission_final)}\")\n\noutput_path = 'submission.csv'\nsubmission_final.to_csv(output_path, index=False)\nprint(f\"\\n✓ Submission saved to: {output_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T06:18:20.324392Z","iopub.execute_input":"2025-11-23T06:18:20.324716Z","iopub.status.idle":"2025-11-23T06:19:07.040263Z","shell.execute_reply.started":"2025-11-23T06:18:20.324689Z","shell.execute_reply":"2025-11-23T06:19:07.039324Z"}},"outputs":[],"execution_count":null}]}