{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":114201,"databundleVersionId":13622514,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":539.653639,"end_time":"2025-09-04T16:19:27.117376","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-09-04T16:10:27.463737","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Simple baseline with Bioclimatic Cubes — ResNet6 + Binary Cross Entropy\n\nTo demonstrate the potential of a single-modality model trained with Bioclimatic cubes, we provide a straightforward baseline based on a custom ResNet6-like architecture and Binary Cross-Entropy. The model itself should learn the relationship between the precise climatic history of a given location and its species composition.\n\nConsidering the significant extent of enhancing the performance of this baseline, we encourage you to experiment with various techniques, architectures, losses, etc.\n\n#### **Have Fun!**","metadata":{"papermill":{"duration":0.004393,"end_time":"2025-09-04T16:10:31.565559","exception":false,"start_time":"2025-09-04T16:10:31.561166","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os\nimport torch\nimport tqdm\nimport numpy as np\nimport pandas as pd\nimport torchvision.models as models\nimport torchvision.transforms as transforms\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom sklearn.metrics import precision_recall_fscore_support","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:07.298310Z","start_time":"2024-04-30T21:25:05.354584Z"},"execution":{"iopub.status.busy":"2025-09-04T17:23:13.956236Z","iopub.execute_input":"2025-09-04T17:23:13.956473Z","iopub.status.idle":"2025-09-04T17:23:23.194754Z","shell.execute_reply.started":"2025-09-04T17:23:13.956449Z","shell.execute_reply":"2025-09-04T17:23:23.194100Z"},"papermill":{"duration":12.117448,"end_time":"2025-09-04T16:10:43.686302","exception":false,"start_time":"2025-09-04T16:10:31.568854","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data description\n\nThe Bioclimatic Cubes were created from **four** monthly GeoTIFF CHELSA (https://chelsa-climate.org/timeseries/) time series climatic rasters with a resolution of 30 arc seconds, i.e., approximately 1km. The four variables are the precipitation (pr), maximum (taxmax), minimum (tasmin), and mean (tax) daily temperatures per month from January 2000 to June 2019. We provide the data in the form of data cubes as a tensor object (.pt). The cubes are structured as follows.\n**Shape**: `(n_year, n_month, n_bio)` where:\n- `n_year` = 19 (ranging from 2000 to 2018)\n- `n_month` = 12 (ranging from January 01 to December 12)\n- `n_bio` = 4 comprising [`pr` (precipitation), `tas` (mean daily air temperature), `tasmin`, `tasmax`]\n\nThe datacubes can simply be loaded as tensors using PyTorch with the following command :\n\n```python\nimport torch\ntorch.load('path_to_file.pt')\n```\n\n**References:**\n- *Karger, D.N., Conrad, O., Böhner, J., Kawohl, T., Kreft, H., Soria-Auza, R.W., Zimmermann, N.E., Linder, P., Kessler, M. (2017): Climatologies at high resolution for the Earth land surface areas. Scientific Data. 4 170122. https://doi.org/10.1038/sdata.2017.122*\n\n- *Karger D.N., Conrad, O., Böhner, J., Kawohl, T., Kreft, H., Soria-Auza, R.W., Zimmermann, N.E, Linder, H.P., Kessler, M. Data from: Climatologies at high resolution for the earth’s land surface areas. Dryad Digital Repository. http://dx.doi.org/doi:10.5061/dryad.kd1d4*","metadata":{"execution":{"iopub.execute_input":"2024-05-01T13:30:07.054038Z","iopub.status.busy":"2024-05-01T13:30:07.053659Z","iopub.status.idle":"2024-05-01T13:30:07.058148Z","shell.execute_reply":"2024-05-01T13:30:07.057269Z","shell.execute_reply.started":"2024-05-01T13:30:07.054008Z"},"papermill":{"duration":0.002986,"end_time":"2025-09-04T16:10:43.692783","exception":false,"start_time":"2025-09-04T16:10:43.689797","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"## Prepare custom dataset loader\n\nWe have to slightly update the Dataset to provide the relevant data in the appropriate format.","metadata":{"papermill":{"duration":0.002837,"end_time":"2025-09-04T16:10:43.698657","exception":false,"start_time":"2025-09-04T16:10:43.695820","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class TrainDataset(Dataset):\n    def __init__(self, data_dir, metadata, subset, transform=None):\n        self.subset = subset\n        self.transform = transform\n        self.data_dir = data_dir\n        self.metadata = metadata\n        self.metadata = self.metadata.dropna(subset=\"speciesId\").reset_index(drop=True)\n        self.metadata['speciesId'] = self.metadata['speciesId'].astype(int)\n        self.label_dict = self.metadata.groupby('surveyId')['speciesId'].apply(list).to_dict()\n        \n        self.metadata = self.metadata.drop_duplicates(subset=\"surveyId\").reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.metadata)\n\n    def __getitem__(self, idx):\n        \n        survey_id = self.metadata.surveyId[idx]\n        sample = torch.load(os.path.join(self.data_dir, f\"GLC25-PA-{self.subset}-bioclimatic_monthly_{survey_id}_cube.pt\"), weights_only=True)\n        species_ids = self.label_dict.get(survey_id, [])  # Get list of species IDs for the survey ID\n        label = torch.zeros(num_classes)  \n        for species_id in species_ids:\n            label_id = species_id\n            label[label_id] = 1  # Set the corresponding class index to 1 for each species\n\n        # Ensure the sample is in the correct format for the transform\n        if isinstance(sample, torch.Tensor):\n            sample = sample.permute(1, 2, 0)  # Change tensor shape from (C, H, W) to (H, W, C)\n            sample = sample.numpy()  \n\n        if self.transform:\n            sample = self.transform(sample)\n\n        return sample, label, survey_id\n    \nclass TestDataset(TrainDataset):\n    def __init__(self, data_dir, metadata, subset, transform=None):\n        self.subset = subset\n        self.transform = transform\n        self.data_dir = data_dir\n        self.metadata = metadata\n        \n    def __getitem__(self, idx):\n        \n        survey_id = self.metadata.surveyId[idx]\n        sample = torch.load(os.path.join(self.data_dir, f\"GLC25-PA-{self.subset}-bioclimatic_monthly_{survey_id}_cube.pt\"), weights_only=True)\n        \n        if isinstance(sample, torch.Tensor):\n            sample = sample.permute(1, 2, 0)  # Change tensor shape from (C, H, W) to (H, W, C)\n            sample = sample.numpy()\n\n        if self.transform:\n            sample = self.transform(sample)\n\n        return sample, survey_id","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:32.627928Z","start_time":"2024-04-30T21:25:32.612131Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:23.196113Z","iopub.execute_input":"2025-09-04T17:23:23.196549Z","iopub.status.idle":"2025-09-04T17:23:23.205940Z","shell.execute_reply.started":"2025-09-04T17:23:23.196507Z","shell.execute_reply":"2025-09-04T17:23:23.205191Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.013543,"end_time":"2025-09-04T16:10:43.715090","exception":false,"start_time":"2025-09-04T16:10:43.701547","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load metadata and prepare data loaders","metadata":{"papermill":{"duration":0.002836,"end_time":"2025-09-04T16:10:43.720957","exception":false,"start_time":"2025-09-04T16:10:43.718121","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Dataset and DataLoader\nbatch_size = 64\ntransform = transforms.Compose([\n    transforms.ToTensor()\n])\n\n# Load Training metadata\ntrain_data_path = \"/kaggle/input/geoplant-at-paiss/BioclimTimeSeries/cubes/PA-train\"\ntrain_metadata_path = \"/kaggle/input/geoplant-at-paiss/GLC25_PA_metadata_train.csv\"\ntrain_metadata = pd.read_csv(train_metadata_path)\ntrain_dataset = TrainDataset(train_data_path, train_metadata, subset=\"train\", transform=transform)\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)\n\n# Load Test metadata\ntest_data_path = \"/kaggle/input/geoplant-at-paiss/BioclimTimeSeries/cubes/PA-test\"\ntest_metadata_path = \"/kaggle/input/geoplant-at-paiss/GLC25_PA_metadata_test.csv\"\ntest_metadata = pd.read_csv(test_metadata_path)\ntest_dataset = TestDataset(test_data_path, test_metadata, subset=\"test\", transform=transform)\ntest_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4)","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:34.532017Z","start_time":"2024-04-30T21:25:32.615562Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:23.206608Z","iopub.execute_input":"2025-09-04T17:23:23.206919Z","iopub.status.idle":"2025-09-04T17:23:26.910157Z","shell.execute_reply.started":"2025-09-04T17:23:23.206891Z","shell.execute_reply":"2025-09-04T17:23:26.908492Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":3.54781,"end_time":"2025-09-04T16:10:47.271756","exception":false,"start_time":"2025-09-04T16:10:43.723946","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define and initialize the custom ResNet6-like architecture\n\nWe use a compact ResNet-6–style encoder tailored to the bioclimatic cubes shaped [4, 19, 12] (channels = 4 variables; spatial grid = 19×12 for years × months). The model\n* accepts 4 input channels and applies per-sample LayerNorm([4,19,12]);\n* uses a 3×3 stem conv with stride=1 (no initial max-pool) to avoid losing information on the small grid;\n* stacks 3 residual blocks (each block has two 3×3 conv layers with identity skip) → 6 conv layers total;\n* finishes with global average pooling and a light MLP head producing logits for multi-label classification (for BCEWithLogitsLoss).","metadata":{"papermill":{"duration":0.002903,"end_time":"2025-09-04T16:10:47.278147","exception":false,"start_time":"2025-09-04T16:10:47.275244","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass BasicBlock(nn.Module):\n    \"\"\"2×Conv 3×3 with identity skip. No downsampling (stride=1).\"\"\"\n    def __init__(self, channels: int):\n        super().__init__()\n        self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn1   = nn.BatchNorm2d(channels)\n        self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn2   = nn.BatchNorm2d(channels)\n\n    def forward(self, x):\n        identity = x\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = F.relu(out, inplace=True)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out += identity\n        out = F.relu(out, inplace=True)\n        return out\n\nclass ResNet6(nn.Module):\n    \"\"\"\n    ResNet-6 for climatic cubes:\n      - Input: [B, 4, 19, 12]\n      - Stem: 3×3 conv (no maxpool)\n      - 3 residual blocks (6 conv layers total)\n      - Global avg pool + MLP head to num_classes\n    \"\"\"\n    def __init__(self, num_classes: int, stem_channels: int = 64, mlp_hidden: int = 512, p_drop: float = 0.1):\n        super().__init__()\n\n        # Normalize the 4×19×12 tensor per-sample (stable on small grids)\n        self.norm_input = nn.LayerNorm([4, 19, 12])\n\n        # Stem: 4 → stem_channels\n        self.stem = nn.Conv2d(4, stem_channels, kernel_size=3, stride=1, padding=1, bias=False)\n        self.stem_bn = nn.BatchNorm2d(stem_channels)\n\n        # 3 residual blocks, no downsampling (keeps 19×12)\n        self.block1 = BasicBlock(stem_channels)\n        self.block2 = BasicBlock(stem_channels)\n        self.block3 = BasicBlock(stem_channels)\n\n        # Global average pooling to [B, C]\n        self.gap = nn.AdaptiveAvgPool2d(1)\n\n        # Head: small MLP → logits\n        self.head = nn.Sequential(\n            nn.Flatten(),              # [B, C, 1, 1] -> [B, C]\n            nn.LayerNorm(stem_channels),\n            nn.Linear(stem_channels, mlp_hidden),\n            nn.ReLU(inplace=True),\n            nn.Dropout(p_drop),\n            nn.Linear(mlp_hidden, num_classes)  # logits for BCEWithLogitsLoss\n        )\n\n        self._init_weights()\n\n    def _init_weights(self):\n        # Kaiming init for convs; zeros for BN/Linear biases\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight, mode=\"fan_out\", nonlinearity=\"relu\")\n            elif isinstance(m, nn.BatchNorm2d):\n                nn.init.ones_(m.weight)\n                nn.init.zeros_(m.bias)\n            elif isinstance(m, nn.Linear):\n                nn.init.trunc_normal_(m.weight, std=0.02)\n                nn.init.zeros_(m.bias)\n\n    def forward(self, x):\n        x = self.norm_input(x)              # [B, 4, 19, 12]\n        x = self.stem(x)                    # [B, C, 19, 12]\n        x = self.stem_bn(x)\n        x = F.relu(x, inplace=True)\n\n        x = self.block1(x)                  # 6 conv layers total across 3 blocks\n        x = self.block2(x)\n        x = self.block3(x)\n\n        x = self.gap(x)                     # [B, C, 1, 1]\n        x = self.head(x)                    # [B, num_classes] (logits)\n        return x","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:31.014067Z","start_time":"2024-04-30T21:25:31.010060Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:26.912823Z","iopub.execute_input":"2025-09-04T17:23:26.913246Z","iopub.status.idle":"2025-09-04T17:23:26.933166Z","shell.execute_reply.started":"2025-09-04T17:23:26.913214Z","shell.execute_reply":"2025-09-04T17:23:26.932258Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.018578,"end_time":"2025-09-04T16:10:47.300480","exception":false,"start_time":"2025-09-04T16:10:47.281902","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(seed):\n    # Set seed for Python's built-in random number generator\n    torch.manual_seed(seed)\n    # Set seed for numpy\n    np.random.seed(seed)\n    # Set seed for CUDA if available\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n        # Set cuDNN's random number generator seed for deterministic behavior\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n\nset_seed(69)","metadata":{"execution":{"iopub.status.busy":"2025-09-04T17:23:26.933953Z","iopub.execute_input":"2025-09-04T17:23:26.934845Z","iopub.status.idle":"2025-09-04T17:23:27.220196Z","shell.execute_reply.started":"2025-09-04T17:23:26.934810Z","shell.execute_reply":"2025-09-04T17:23:27.219630Z"},"papermill":{"duration":0.063504,"end_time":"2025-09-04T16:10:47.367008","exception":false,"start_time":"2025-09-04T16:10:47.303504","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check if cuda is available\ndevice = torch.device(\"cpu\")\n\nif torch.cuda.is_available():\n    device = torch.device(\"cuda\")\n    print(\"DEVICE = CUDA\")\n\nnum_classes = 11255 # Number of all unique classes within the PO and PA data.\nmodel = ResNet6(num_classes).to(device)","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:31.611823Z","start_time":"2024-04-30T21:25:31.607373Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:27.221118Z","iopub.execute_input":"2025-09-04T17:23:27.221359Z","iopub.status.idle":"2025-09-04T17:23:27.543055Z","shell.execute_reply.started":"2025-09-04T17:23:27.221339Z","shell.execute_reply":"2025-09-04T17:23:27.542402Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.262475,"end_time":"2025-09-04T16:10:47.633006","exception":false,"start_time":"2025-09-04T16:10:47.370531","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Loop\n\nNothing special, just a standard Pytorch training loop.","metadata":{"papermill":{"duration":0.00291,"end_time":"2025-09-04T16:10:47.639234","exception":false,"start_time":"2025-09-04T16:10:47.636324","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Hyperparameters\nlearning_rate = 0.0005\nnum_epochs = 10\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)\nscheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, verbose=True)","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:32.181927Z","start_time":"2024-04-30T21:25:32.177073Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:27.543709Z","iopub.execute_input":"2025-09-04T17:23:27.543893Z","iopub.status.idle":"2025-09-04T17:23:27.550776Z","shell.execute_reply.started":"2025-09-04T17:23:27.543878Z","shell.execute_reply":"2025-09-04T17:23:27.549974Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.011012,"end_time":"2025-09-04T16:10:47.653213","exception":false,"start_time":"2025-09-04T16:10:47.642201","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nfrom tqdm import tqdm\nimport torch\n\nprint(f\"Training for {num_epochs} epochs started.\")\nstart_time = time.time()\n\nfor epoch in range(num_epochs):\n    epoch_start = time.time()\n    model.train()\n\n    running_loss = 0.0\n    pbar = tqdm(enumerate(train_loader), total=len(train_loader), desc=f\"Epoch {epoch+1}/{num_epochs}\")\n\n    for batch_idx, (data, targets, _) in pbar:\n        data = data.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(data)\n\n        pos_weight = targets\n        criterion = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        loss = criterion(outputs, targets)\n\n        loss.backward()\n        optimizer.step()\n\n        # Update running loss\n        running_loss += loss.item()\n        avg_loss = running_loss / (batch_idx + 1)\n\n        # Show loss in the tqdm bar\n        pbar.set_postfix({\n            \"batch_loss\": f\"{loss.item():.4f}\",\n            \"avg_loss\": f\"{avg_loss:.4f}\"\n        })\n\n    scheduler.step()\n    epoch_time = time.time() - epoch_start\n    print(f\"\\nEpoch {epoch+1} finished in {epoch_time:.2f} seconds\")\n    print(\"Scheduler:\", scheduler.state_dict())\n\n# Save the trained model\nmodel.eval()\ntorch.save(model.state_dict(), \"resnet6-with-bioclimatic-cubes.pth\")\n\ntotal_time = time.time() - start_time\nprint(f\"Training completed in {total_time/60:.2f} minutes\")","metadata":{"ExecuteTime":{"start_time":"2024-04-30T21:25:34.536634Z"},"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:23:27.551673Z","iopub.execute_input":"2025-09-04T17:23:27.552480Z","iopub.status.idle":"2025-09-04T17:30:48.653267Z","shell.execute_reply.started":"2025-09-04T17:23:27.552451Z","shell.execute_reply":"2025-09-04T17:30:48.652446Z"},"is_executing":true,"jupyter":{"outputs_hidden":false},"papermill":{"duration":484.27311,"end_time":"2025-09-04T16:18:51.929389","exception":false,"start_time":"2025-09-04T16:10:47.656279","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test Loop\n\nAgain, nothing special, just a standard inference. ","metadata":{"papermill":{"duration":0.741853,"end_time":"2025-09-04T16:18:53.362771","exception":false,"start_time":"2025-09-04T16:18:52.620918","status":"completed"},"tags":[]}},{"cell_type":"code","source":"with torch.no_grad():\n    all_predictions = []\n    surveys = []\n    top_k_indices = None\n    for data, surveyID in tqdm(test_loader, total=len(test_loader)):\n\n        data = data.to(device)\n        \n        outputs = model(data)\n        predictions = torch.sigmoid(outputs).cpu().numpy()\n\n        # Select top-25 values as predictions\n        top_25 = np.argsort(-predictions, axis=1)[:, :25] \n        if top_k_indices is None:\n            top_k_indices = top_25\n        else:\n            top_k_indices = np.concatenate((top_k_indices, top_25), axis=0)\n\n        surveys.extend(surveyID.cpu().numpy())","metadata":{"collapsed":false,"execution":{"iopub.status.busy":"2025-09-04T17:30:48.654370Z","iopub.execute_input":"2025-09-04T17:30:48.654689Z","iopub.status.idle":"2025-09-04T17:31:09.604828Z","shell.execute_reply.started":"2025-09-04T17:30:48.654659Z","shell.execute_reply":"2025-09-04T17:31:09.603858Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":25.098264,"end_time":"2025-09-04T16:19:19.211313","exception":false,"start_time":"2025-09-04T16:18:54.113049","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Save prediction file! 🎉🥳🙌🤗","metadata":{"papermill":{"duration":0.749291,"end_time":"2025-09-04T16:19:20.652066","exception":false,"start_time":"2025-09-04T16:19:19.902775","status":"completed"},"tags":[]}},{"cell_type":"code","source":"data_concatenated = [' '.join(map(str, row)) for row in top_k_indices]\n\npd.DataFrame(\n    {'surveyId': surveys,\n     'predictions': data_concatenated,\n    }).to_csv(\"submission.csv\", index = False)","metadata":{"execution":{"iopub.status.busy":"2025-09-04T17:31:09.607181Z","iopub.execute_input":"2025-09-04T17:31:09.607745Z","iopub.status.idle":"2025-09-04T17:31:09.810387Z","shell.execute_reply.started":"2025-09-04T17:31:09.607715Z","shell.execute_reply":"2025-09-04T17:31:09.809782Z"},"papermill":{"duration":0.876437,"end_time":"2025-09-04T16:19:22.274488","exception":false,"start_time":"2025-09-04T16:19:21.398051","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"papermill":{"duration":0.754345,"end_time":"2025-09-04T16:19:23.787019","exception":false,"start_time":"2025-09-04T16:19:23.032674","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}