{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14418452,"sourceType":"datasetVersion","datasetId":9208870,"isSourceIdPinned":true},{"sourceId":14470426,"sourceType":"datasetVersion","datasetId":9242375},{"sourceId":14470549,"sourceType":"datasetVersion","datasetId":9208863,"isSourceIdPinned":true}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# !pip install -q --upgrade timm>=1.0.0","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-14T00:36:27.244488Z","iopub.execute_input":"2026-01-14T00:36:27.245294Z","iopub.status.idle":"2026-01-14T00:36:27.249187Z","shell.execute_reply.started":"2026-01-14T00:36:27.245258Z","shell.execute_reply":"2026-01-14T00:36:27.248402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =============================================================================\n# 1. Imports & Configuration (for Kaggle environment)\n# =============================================================================\nimport os\nimport gc\nimport random\nfrom pathlib import Path\nfrom dataclasses import dataclass\nfrom typing import Optional\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\n\n\n@dataclass\nclass InferenceConfig:\n    \"\"\"Inference configuration (for Kaggle environment)\"\"\"\n    # Paths (Kaggle environment)\n    DATA_PATH: Path = Path('/kaggle/input/csiro-biomass')\n    MODEL_PATH: Path = Path('/kaggle/input/csiro-dinov3-models')  # DINOv3 model dataset\n    SIGLIP_PREDS_PATH: Path = Path('/kaggle/input/csiro-siglip-preds')  # SigLIP prediction dataset\n    OUTPUT_PATH: Path = Path('/kaggle/working')\n    \n    # Model\n    BACKBONE_NAME: str = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    IMG_SIZE: int = 512\n    N_FOLDS: int = 5\n    \n    # Inference\n    BATCH_SIZE: int = 4  # Adjust to Kaggle GPU memory\n    NUM_WORKERS: int = 0\n    DEVICE: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n    USE_TTA: bool = True\n    TTA_MODES: tuple = ('original', 'hflip', 'vflip', 'both')\n    \n    # Ensemble\n    ALPHA: float = 0.6  # Weight for DINOv3 (1-ALPHA = weight for SigLIP)\n    \n    # Target columns order\n    DINO_TARGET_ORDER: tuple = ('Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g')\n    SIGLIP_TARGET_ORDER: tuple = ('Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g')\n    FINAL_TARGET_ORDER: tuple = ('Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g')\n    \n    # All target columns (for submission)\n    ALL_TARGET_COLS: tuple = ('Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g')\n    \n    SEED: int = 42\n\n\nCFG = InferenceConfig()\n\n# Create output directory\nCFG.OUTPUT_PATH.mkdir(parents=True, exist_ok=True)\n\nprint(f'Device: {CFG.DEVICE}')\nprint(f'Data path: {CFG.DATA_PATH}')\nprint(f'Model path: {CFG.MODEL_PATH}')\nprint(f'SigLIP preds path: {CFG.SIGLIP_PREDS_PATH}')\nprint(f'Output path: {CFG.OUTPUT_PATH}')\n\n\n# =============================================================================\n# 2. Seed & Utilities\n# =============================================================================\ndef seed_everything(seed: int) -> None:\n    \"\"\"Seed setup for reproducibility\"\"\"\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n        torch.cuda.manual_seed_all(seed)\n        torch.backends.cudnn.deterministic = True\n\n\ndef flush() -> None:\n    \"\"\"Free GPU memory\"\"\"\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\n\ndef clean_values(arr: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Convert NaN, inf, -inf to 0.0 and clip negative values to 0\n    \n    Args:\n        arr: Input array\n        \n    Returns:\n        Cleaned array\n    \"\"\"\n    arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)\n    arr = np.maximum(arr, 0.0)\n    return arr\n\n\nseed_everything(CFG.SEED)\nprint(f'Seed: {CFG.SEED}')\n\n\n# =============================================================================\n# 3. Data Loading\n# =============================================================================\ndef load_test_data(data_path: Path) -> tuple[pd.DataFrame, pd.DataFrame]:\n    \"\"\"\n    Load test data\n    \n    Args:\n        data_path: Data path\n        \n    Returns:\n        test_long: Original long-format test data (for preserving sample_id)\n        test_wide: Wide-format test data (for inference)\n    \"\"\"\n    # Preserve the original long format (to obtain sample_id accurately)\n    test_long = pd.read_csv(data_path / 'test.csv')\n    \n    # Convert to wide format (for inference)\n    test_wide = test_long.copy()\n    test_wide['target'] = 0\n    test_wide = pd.pivot_table(\n        test_wide, values='target', index='image_path',\n        columns='target_name', aggfunc='mean'\n    ).reset_index()\n    test_wide['full_path'] = test_wide['image_path'].apply(lambda p: str(data_path / p))\n    \n    return test_long, test_wide\n\n\ntest_long, test_df = load_test_data(CFG.DATA_PATH)\nprint(f'Test samples (long): {len(test_long)}')\nprint(f'Test samples (wide/unique images): {len(test_df)}')\nprint(test_df.head())\n\n\n# =============================================================================\n# 4. Model Definition\n# =============================================================================\nclass LocalMambaBlock(nn.Module):\n    \"\"\"Lightweight Mamba-like block\"\"\"\n    \n    def __init__(self, dim: int, kernel_size: int = 5, dropout: float = 0.1):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size,\n                                 padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = x * g\n        x = x.transpose(1, 2)\n        x = self.dwconv(x)\n        x = x.transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass BiomassModel(nn.Module):\n    \"\"\"DINOv3 + Mamba Fusion + Multi-Head Regression\"\"\"\n    \n    def __init__(self, model_name: str, pretrained: bool = True,\n                 backbone_path: Optional[Path] = None):\n        super().__init__()\n        self.model_name = model_name\n        self.backbone_path = backbone_path\n        \n        self.backbone = timm.create_model(\n            model_name, pretrained=False, num_classes=0, global_pool=''\n        )\n        nf = self.backbone.num_features\n        \n        self.fusion = nn.Sequential(\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1)\n        )\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        \n        self.head_green = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n        self.head_dead = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n        self.head_clover = nn.Sequential(\n            nn.Linear(nf, nf // 2), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(nf // 2, 1), nn.Softplus()\n        )\n    \n    def forward(self, left: torch.Tensor, right: torch.Tensor\n    ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:\n        x_l = self.backbone(left)\n        x_r = self.backbone(right)\n        x_cat = torch.cat([x_l, x_r], dim=1)\n        x_fused = self.fusion(x_cat)\n        x_pool = self.pool(x_fused.transpose(1, 2)).flatten(1)\n        \n        green = self.head_green(x_pool)\n        dead = self.head_dead(x_pool)\n        clover = self.head_clover(x_pool)\n        gdm = green + clover\n        total = gdm + dead\n        return green, dead, clover, gdm, total\n\n\nprint('Model definition loaded.')\n\n\n# =============================================================================\n# 5. Dataset & Transforms\n# =============================================================================\ndef clean_image(img: np.ndarray) -> np.ndarray:\n    \"\"\"Image preprocessing: crop the bottom 10% to remove the date stamp\"\"\"\n    h, w = img.shape[:2]\n    img = img[0:int(h * 0.90), :]\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lower = np.array([5, 150, 150])\n    upper = np.array([25, 255, 255])\n    mask = cv2.inRange(hsv, lower, upper)\n    mask = cv2.dilate(mask, np.ones((3, 3), np.uint8), iterations=2)\n    if np.sum(mask) > 0:\n        img = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)\n    return img\n\n\ndef get_tta_transforms(img_size: int, mode: str) -> A.Compose:\n    \"\"\"Transforms for TTA\"\"\"\n    transforms_list = [A.Resize(img_size, img_size)]\n    if mode == 'hflip':\n        transforms_list.append(A.HorizontalFlip(p=1.0))\n    elif mode == 'vflip':\n        transforms_list.append(A.VerticalFlip(p=1.0))\n    elif mode == 'both':\n        transforms_list.append(A.HorizontalFlip(p=1.0))\n        transforms_list.append(A.VerticalFlip(p=1.0))\n    transforms_list.extend([\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n    return A.Compose(transforms_list)\n\n\nclass TestDataset(torch.utils.data.Dataset):\n    \"\"\"Test dataset\"\"\"\n    \n    def __init__(self, df: pd.DataFrame, img_size: int, tta_mode: str = 'original'):\n        self.df = df.reset_index(drop=True)\n        self.transform = get_tta_transforms(img_size, tta_mode)\n    \n    def __len__(self) -> int:\n        return len(self.df)\n    \n    def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor, str]:\n        row = self.df.iloc[idx]\n        img = cv2.imread(row['full_path'])\n        if img is None:\n            # Use a dummy image if the image cannot be read\n            print(f\"Warning: Could not read image {row['full_path']}\")\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        h, w, _ = img.shape\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n        left_t = self.transform(image=left)['image']\n        right_t = self.transform(image=right)['image']\n        return left_t, right_t, row['image_path']\n\n\nprint('Dataset defined.')\n\n\n# =============================================================================\n# 6. DINOv3 Inference Functions\n# =============================================================================\ndef load_dino_model(model_path: Path, device: str) -> BiomassModel:\n    \"\"\"Load the trained DINOv3 model\"\"\"\n    model = BiomassModel(model_name=CFG.BACKBONE_NAME, pretrained=False, backbone_path=None)\n    state_dict = torch.load(model_path, map_location=device, weights_only=True)\n    model.load_state_dict(state_dict)\n    model = model.to(device)\n    model.eval()\n    return model\n\n\n@torch.no_grad()\ndef predict_dino_fold(model: BiomassModel, dataloader: torch.utils.data.DataLoader,\n                      device: str) -> np.ndarray:\n    \"\"\"Prediction for a single fold\"\"\"\n    preds = []\n    for left, right, _ in tqdm(dataloader, desc='Predicting', leave=False):\n        left = left.to(device, non_blocking=True)\n        right = right.to(device, non_blocking=True)\n        green, dead, clover, gdm, total = model(left, right)\n        pred = torch.stack([\n            green.squeeze(-1), dead.squeeze(-1), clover.squeeze(-1),\n            gdm.squeeze(-1), total.squeeze(-1)\n        ], dim=1)\n        preds.append(pred.cpu().numpy())\n    \n    result = np.vstack(preds)\n    # Add NaN handling\n    result = clean_values(result)\n    return result\n\n\ndef run_dino_inference(test_df: pd.DataFrame, model_path: Path,\n                       n_folds: int, use_tta: bool = True) -> np.ndarray:\n    \"\"\"DINOv3 inference (TTA + fold averaging)\"\"\"\n    all_fold_preds = []\n    \n    for fold in range(n_folds):\n        fold_model_path = model_path / f'dinov3_fold{fold}.pth'\n        if not fold_model_path.exists():\n            print(f'Warning: {fold_model_path} not found, skipping fold {fold}')\n            continue\n        \n        print(f'\\n===== Fold {fold} =====')\n        model = load_dino_model(fold_model_path, CFG.DEVICE)\n        \n        tta_preds = []\n        tta_modes = CFG.TTA_MODES if use_tta else ('original',)\n        \n        for tta_mode in tta_modes:\n            print(f'  TTA: {tta_mode}')\n            dataset = TestDataset(test_df, CFG.IMG_SIZE, tta_mode=tta_mode)\n            dataloader = torch.utils.data.DataLoader(\n                dataset, batch_size=CFG.BATCH_SIZE, shuffle=False,\n                num_workers=CFG.NUM_WORKERS, pin_memory=True\n            )\n            preds = predict_dino_fold(model, dataloader, CFG.DEVICE)\n            tta_preds.append(preds)\n        \n        fold_pred = np.mean(tta_preds, axis=0)\n        fold_pred = clean_values(fold_pred)  # NaN handling\n        all_fold_preds.append(fold_pred)\n        del model\n        flush()\n    \n    if not all_fold_preds:\n        raise ValueError('No fold models found!')\n    \n    result = np.mean(all_fold_preds, axis=0)\n    result = clean_values(result)  # Final NaN handling\n    return result\n\n\nprint('DINOv3 inference functions defined.')\n\n\n# =============================================================================\n# 7. Run DINOv3 Inference\n# =============================================================================\nprint('Running DINOv3 inference...')\ndino_preds = run_dino_inference(\n    test_df=test_df, model_path=CFG.MODEL_PATH,\n    n_folds=CFG.N_FOLDS, use_tta=CFG.USE_TTA\n)\nprint(f'\\nDINOv3 predictions shape: {dino_preds.shape}')\n\n\n# =============================================================================\n# 8. Load SigLIP+GBDT Predictions\n# =============================================================================\ndef load_siglip_predictions(preds_path: Path, n_samples: int) -> Optional[np.ndarray]:\n    \"\"\"\n    Load SigLIP+GBDT prediction results\n    \n    Args:\n        preds_path: Path to the prediction file\n        n_samples: Expected number of samples\n        \n    Returns:\n        Prediction array, or None if loading fails\n    \"\"\"\n    npy_path = preds_path / 'test_preds_siglip.npy'\n    \n    if not npy_path.exists():\n        print(f'Warning: {npy_path} not found.')\n        return None\n    \n    try:\n        preds = np.load(npy_path)\n        preds = clean_values(preds)  # NaN handling\n        \n        # Check number of samples\n        if preds.shape[0] != n_samples:\n            print(f'Warning: SigLIP preds shape mismatch. Expected {n_samples}, got {preds.shape[0]}')\n            return None\n        \n        return preds\n    except Exception as e:\n        print(f'Error loading SigLIP predictions: {e}')\n        return None\n\n\nsiglip_preds = load_siglip_predictions(CFG.SIGLIP_PREDS_PATH, len(test_df))\n\nif siglip_preds is not None:\n    print(f'SigLIP+GBDT predictions shape: {siglip_preds.shape}')\nelse:\n    print('SigLIP predictions not available. Using DINOv3 only.')\n\n\n# =============================================================================\n# 9. Ensemble\n# =============================================================================\ndef reorder_predictions(preds: np.ndarray, source_order: tuple[str, ...],\n                        target_order: tuple[str, ...]) -> np.ndarray:\n    \"\"\"Convert the column order of predictions\"\"\"\n    if source_order == target_order:\n        return preds\n    indices = [source_order.index(col) for col in target_order]\n    return preds[:, indices]\n\n\ndef ensemble_predictions(dino_preds: np.ndarray, siglip_preds: Optional[np.ndarray],\n                         alpha: float, dino_order: tuple[str, ...],\n                         siglip_order: tuple[str, ...],\n                         final_order: tuple[str, ...]) -> np.ndarray:\n    \"\"\"\n    Ensemble predictions from two models\n    \n    If SigLIP predictions are unavailable, use DINOv3 only\n    \"\"\"\n    dino_reordered = reorder_predictions(dino_preds, dino_order, final_order)\n    \n    if siglip_preds is None:\n        # If SigLIP predictions are unavailable, use DINOv3 only\n        print('Using DINOv3 predictions only (no SigLIP).')\n        return clean_values(dino_reordered)\n    \n    siglip_reordered = reorder_predictions(siglip_preds, siglip_order, final_order)\n    result = alpha * dino_reordered + (1 - alpha) * siglip_reordered\n    return clean_values(result)\n\n\nensemble_preds = ensemble_predictions(\n    dino_preds=dino_preds, siglip_preds=siglip_preds, alpha=CFG.ALPHA,\n    dino_order=CFG.DINO_TARGET_ORDER, siglip_order=CFG.SIGLIP_TARGET_ORDER,\n    final_order=CFG.FINAL_TARGET_ORDER\n)\nprint(f'Ensemble predictions shape: {ensemble_preds.shape}')\n\n\n# =============================================================================\n# 10. Post-Processing\n# =============================================================================\ndef post_process_biomass(predictions: np.ndarray) -> np.ndarray:\n    \"\"\"Adjust predictions to satisfy physical constraints\"\"\"\n    current_order = list(CFG.FINAL_TARGET_ORDER)\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    \n    reorder_to_constraint = [\n        current_order.index('Dry_Green_g'), current_order.index('Dry_Clover_g'),\n        current_order.index('Dry_Dead_g'), current_order.index('GDM_g'),\n        current_order.index('Dry_Total_g')\n    ]\n    Y = predictions[:, reorder_to_constraint].T\n    \n    C = np.array([\n        [1, 1, 0, -1, 0],   # Green + Clover - GDM = 0\n        [0, 0, 1, 1, -1]    # Dead + GDM - Total = 0\n    ])\n    P = np.eye(5) - C.T @ np.linalg.inv(C @ C.T) @ C\n    Y_reconciled = (P @ Y).T.clip(min=0)\n    \n    reorder_back = [ordered_cols.index(col) for col in current_order]\n    result = Y_reconciled[:, reorder_back]\n    \n    # Final NaN handling\n    result = clean_values(result)\n    return result\n\n\nfinal_preds = post_process_biomass(ensemble_preds)\n\n# Verify constraints\nclover_idx = list(CFG.FINAL_TARGET_ORDER).index('Dry_Clover_g')\ndead_idx = list(CFG.FINAL_TARGET_ORDER).index('Dry_Dead_g')\ngreen_idx = list(CFG.FINAL_TARGET_ORDER).index('Dry_Green_g')\ntotal_idx = list(CFG.FINAL_TARGET_ORDER).index('Dry_Total_g')\ngdm_idx = list(CFG.FINAL_TARGET_ORDER).index('GDM_g')\n\ngdm_check = final_preds[:, green_idx] + final_preds[:, clover_idx] - final_preds[:, gdm_idx]\ntotal_check = final_preds[:, gdm_idx] + final_preds[:, dead_idx] - final_preds[:, total_idx]\nprint(f'GDM constraint violation (max): {np.abs(gdm_check).max():.6f}')\nprint(f'Total constraint violation (max): {np.abs(total_check).max():.6f}')\n\n\n# =============================================================================\n# 11. Create Submission (Revised)\n# =============================================================================\ndef create_submission(\n    final_pred: np.ndarray,\n    test_long: pd.DataFrame,\n    test_unique: pd.DataFrame,\n    target_order: tuple[str, ...],\n    output_path: Path\n) -> pd.DataFrame:\n    \"\"\"\n    Create a submission CSV file (same approach as the first notebook)\n    \n    Args:\n        final_pred: Final predictions (n_samples, n_targets)\n        test_long: Original long-format test data (preserves sample_id)\n        test_unique: DataFrame with unique image paths\n        target_order: Column order of predictions\n        output_path: Output path\n        \n    Returns:\n        submission DataFrame\n    \"\"\"\n    # Map predictions to column names\n    # FINAL_TARGET_ORDER: ('Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g')\n    pred_dict = {\n        'image_path': test_unique['image_path'].values,\n    }\n    for i, col in enumerate(target_order):\n        pred_dict[col] = clean_values(final_pred[:, i])\n    \n    wide_df = pd.DataFrame(pred_dict)\n    \n    # Convert from wide format to long format\n    long_preds = wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=list(target_order),\n        var_name='target_name',\n        value_name='target',\n    )\n    \n    # Merge with the original test_long to obtain sample_id accurately\n    sub = pd.merge(\n        test_long[['sample_id', 'image_path', 'target_name']],\n        long_preds,\n        on=['image_path', 'target_name'],\n        how='left',\n    )[['sample_id', 'target']]\n    \n    # Final NaN handling (important!)\n    sub['target'] = sub['target'].fillna(0.0)\n    sub['target'] = np.nan_to_num(sub['target'].values, nan=0.0, posinf=0.0, neginf=0.0)\n    sub['target'] = np.maximum(sub['target'].values, 0.0)\n    \n    # Save\n    submission_path = output_path / 'submission.csv'\n    sub.to_csv(submission_path, index=False)\n    print(f'Saved: {submission_path}')\n    \n    return sub\n\n\nsubmission = create_submission(\n    final_pred=final_preds,\n    test_long=test_long,\n    test_unique=test_df,\n    target_order=CFG.FINAL_TARGET_ORDER,\n    output_path=CFG.OUTPUT_PATH\n)\nprint(f'\\nSubmission shape: {submission.shape}')\nprint('\\nFirst 10 rows:')\nprint(submission.head(10))\n\n# Check NaNs\nnan_count = submission['target'].isna().sum()\nprint(f'\\nNaN count in submission: {nan_count}')\n\n# Check negative values\nneg_count = (submission['target'] < 0).sum()\nprint(f'Negative values in submission: {neg_count}')\n\n\n# =============================================================================\n# 12. Summary Statistics\n# =============================================================================\nprint('\\n===== Prediction Statistics =====')\nfor i, target in enumerate(CFG.FINAL_TARGET_ORDER):\n    vals = final_preds[:, i]\n    print(f'{target}: Mean={vals.mean():.4f}, Std={vals.std():.4f}, '\n          f'Min={vals.min():.4f}, Max={vals.max():.4f}')\n\nprint('\\n===== Ensemble Summary =====')\nprint(f'DINOv3 weight (alpha): {CFG.ALPHA}')\nprint(f'SigLIP weight (1-alpha): {1 - CFG.ALPHA}')\nprint(f'SigLIP available: {siglip_preds is not None}')\nprint(f'TTA enabled: {CFG.USE_TTA}')\nprint(f'Number of folds: {CFG.N_FOLDS}')\n\nprint('\\n===== Validation Checks =====')\nprint(f'Submission rows: {len(submission)}')\nprint(f'Expected rows (from test_long): {len(test_long)}')\nprint(f'Match: {len(submission) == len(test_long)}')\n\nprint('\\n✓ Inference complete!')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T00:36:27.412349Z","iopub.execute_input":"2026-01-14T00:36:27.412607Z","iopub.status.idle":"2026-01-14T00:40:08.871381Z","shell.execute_reply.started":"2026-01-14T00:36:27.412582Z","shell.execute_reply":"2026-01-14T00:40:08.870687Z"}},"outputs":[],"execution_count":null}]}