{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":6.756759,"end_time":"2025-02-26T02:09:27.363350","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-02-26T02:09:20.606591","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# PhysioNet - Digitization of ECG Images\n- Extract the ECG time-series data from scans and photographs of paper printouts of the ECGs.\n### https://www.kaggle.com/competitions/physionet-ecg-image-digitization/","metadata":{}},{"cell_type":"markdown","source":"## Import Required Libraries\nLoading all necessary packages for data processing, visualization, and deep learning with PyTorch.","metadata":{}},{"cell_type":"code","source":"# Import libraries\nimport os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:32.463497Z","iopub.execute_input":"2025-11-01T06:14:32.464198Z","iopub.status.idle":"2025-11-01T06:14:36.374292Z","shell.execute_reply.started":"2025-11-01T06:14:32.464161Z","shell.execute_reply":"2025-11-01T06:14:36.373279Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Configuration and Device Setup\nSetting up data directories and detecting available GPU/CPU for training.","metadata":{}},{"cell_type":"code","source":"# Paths and config\nDATA_DIR = '/kaggle/input/physionet-ecg-image-digitization'\nTRAIN_DIR = f'{DATA_DIR}/train'\nTEST_DIR = f'{DATA_DIR}/test'\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Using device: {DEVICE}')\n\nBATCH_SIZE = 8 \nEPOCHS = 1\nLR = 0.0001","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:36.375843Z","iopub.execute_input":"2025-11-01T06:14:36.376140Z","iopub.status.idle":"2025-11-01T06:14:36.466523Z","shell.execute_reply.started":"2025-11-01T06:14:36.376121Z","shell.execute_reply":"2025-11-01T06:14:36.465794Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Metadata and Split Dataset","metadata":{}},{"cell_type":"code","source":"# Load metadata\nfrom sklearn.model_selection import train_test_split\n\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\ntest_df = pd.read_csv(f'{DATA_DIR}/test.csv')\n\n# Get available record IDs from disk\navailable_train_ids = [int(d) for d in os.listdir(TRAIN_DIR) if os.path.isdir(os.path.join(TRAIN_DIR, d))]\navailable_test_ids = [int(f.replace('.png', '')) for f in os.listdir(TEST_DIR) if f.endswith('.png')]\n\nprint(f'Original train samples in csv: {len(train_df)}, available on disk: {len(available_train_ids)}')\nprint(f'Original test samples in csv: {len(test_df)}, available on disk: {len(available_test_ids)}')\n\n# Filter to only use available samples\ntrain_df = train_df[train_df['id'].isin(available_train_ids)].reset_index(drop=True)\ntest_df = test_df[test_df['id'].isin(available_test_ids)].reset_index(drop=True)\n\nprint(f'\\nFiltered train samples: {len(train_df)}')\nprint(f'Filtered test samples: {len(test_df)}')\n\n# Split train into train and validation (80/20)\nif len(train_df) > 0:\n    train_df, val_df = train_test_split(train_df, test_size=0.2, random_state=42)\n    train_df = train_df.reset_index(drop=True)\n    val_df = val_df.reset_index(drop=True)\n    print(f'\\nAfter 80/20 split:')\n    print(f'Train samples: {len(train_df)}')\n    print(f'Validation samples: {len(val_df)}')\nelse:\n    val_df = pd.DataFrame()\n    print('\\nWARNING: No training data available!')\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:36.467164Z","iopub.execute_input":"2025-11-01T06:14:36.467388Z","iopub.status.idle":"2025-11-01T06:14:37.863568Z","shell.execute_reply.started":"2025-11-01T06:14:36.467368Z","shell.execute_reply":"2025-11-01T06:14:37.862953Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ECG Dataset Class\nCustom PyTorch dataset to load ECG images and ground truth signals. Images are resized from original size (varies) to 256x256 pixels, then normalized to [0,1]. Signals are interpolated to a standard length for consistent batching.","metadata":{}},{"cell_type":"code","source":"# Dataset class for ECG images\nclass ECGDataset(Dataset):\n    def __init__(self, df, data_dir, is_train=True, img_size=(256, 256), standard_len=10000):\n        self.df = df\n        self.data_dir = data_dir\n        self.is_train = is_train\n        self.img_size = img_size\n        self.standard_len = standard_len  # Standard length for all leads via interpolation\n        self.leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n\n    def __len__(self):\n        return len(self.df)\n    \n    def interpolate_signal(self, signal, target_length):\n        \"\"\"Interpolate signal to target length using linear interpolation\"\"\"\n        if len(signal) == target_length:\n            return signal\n        \n        # Create indices for interpolation\n        old_indices = np.linspace(0, len(signal) - 1, len(signal))\n        new_indices = np.linspace(0, len(signal) - 1, target_length)\n        \n        # Linear interpolation\n        interpolated = np.interp(new_indices, old_indices, signal)\n        return interpolated\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        record_id = str(row['id'])\n\n        try:\n            if self.is_train:\n                # Train: images are in subdirectories with multiple pages\n                img_path = f'{self.data_dir}/{record_id}/{record_id}-0001.png'\n            else:\n                # Test: images are directly in the test directory\n                img_path = f'{self.data_dir}/{record_id}.png'\n\n            # Load and preprocess image\n            if not os.path.exists(img_path):\n                raise FileNotFoundError(f\"Image not found: {img_path}\")\n                \n            img = Image.open(img_path).convert('RGB')\n            img = img.resize(self.img_size)\n            img = np.array(img) / 255.0  # Normalize to [0, 1]\n            img = torch.FloatTensor(img).permute(2, 0, 1)  # (C, H, W)\n\n            if self.is_train:\n                # Load ground truth CSV\n                csv_path = f'{self.data_dir}/{record_id}/{record_id}.csv'\n                if not os.path.exists(csv_path):\n                    raise FileNotFoundError(f\"CSV not found: {csv_path}\")\n                    \n                signal_df = pd.read_csv(csv_path)\n\n                # Process each lead separately and interpolate to standard_len\n                processed_leads = []\n                for lead in self.leads:\n                    if lead not in signal_df.columns:\n                        raise ValueError(f\"Lead {lead} not found in CSV for record {record_id}\")\n                        \n                    # Get non-null values for this lead\n                    lead_signal = signal_df[lead].dropna().values\n                    \n                    # Interpolate to standard length\n                    interpolated = self.interpolate_signal(lead_signal, self.standard_len)\n                    processed_leads.append(interpolated)\n\n                # Stack all leads into shape (12, standard_len)\n                signal = np.stack(processed_leads, axis=0)\n                signal = torch.FloatTensor(signal)\n\n                # Get metadata\n                fs = row['fs']\n                sig_len = row['sig_len']\n\n                return img, signal, fs, sig_len, record_id\n            else:\n                # For test, just return image and record_id\n                return img, record_id\n                \n        except Exception as e:\n            print(f\"Error loading record {record_id}: {str(e)}\")\n            raise","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:37.864291Z","iopub.execute_input":"2025-11-01T06:14:37.864530Z","iopub.status.idle":"2025-11-01T06:14:37.874178Z","shell.execute_reply.started":"2025-11-01T06:14:37.864504Z","shell.execute_reply":"2025-11-01T06:14:37.873550Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Simple CNN Model Architecture\n","metadata":{}},{"cell_type":"code","source":"# Simple CNN model\nclass SimpleCNN(nn.Module):\n    def __init__(self, standard_len=2500, num_leads=12):\n        super(SimpleCNN, self).__init__()\n        self.standard_len = standard_len\n        self.num_leads = num_leads\n        \n        self.conv = nn.Sequential(\n            nn.Conv2d(3, 32, 3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n            \n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n            \n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n        )\n        \n        # After 3 maxpool layers: 256/8 = 32, 32/8 = 32 -> 32x32x128\n        self.fc = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(32*32*128, 2048),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(2048, num_leads * standard_len)\n        )\n        \n    def forward(self, x):\n        x = self.conv(x)\n        x = self.fc(x)\n        # Reshape to (batch, num_leads, standard_len)\n        x = x.view(-1, self.num_leads, self.standard_len)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:37.875733Z","iopub.execute_input":"2025-11-01T06:14:37.875984Z","iopub.status.idle":"2025-11-01T06:14:37.888746Z","shell.execute_reply.started":"2025-11-01T06:14:37.875967Z","shell.execute_reply":"2025-11-01T06:14:37.888257Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Verify Dataset Loading\nCreating train/validation datasets with standard length of 2500 samples per lead. Testing data loading and checking shapes are correct.","metadata":{}},{"cell_type":"code","source":"# Test the updated dataset with interpolation\ntrain_subset = train_df.copy()\nval_subset = val_df.copy()\nprint(f'Training on {len(train_subset)} samples')\nprint(f'Validating on {len(val_subset)} samples')\n\n# Create datasets - both use TRAIN_DIR since validation is split from training data\nSTANDARD_LEN = 2500  # Standard length for all leads via interpolation (most efficient)\ntrain_dataset = ECGDataset(train_subset, TRAIN_DIR, is_train=True, standard_len=STANDARD_LEN)\nval_dataset = ECGDataset(val_subset, TRAIN_DIR, is_train=True, standard_len=STANDARD_LEN)\n\n# Check one sample from train\nif len(train_dataset) > 0:\n    img, signal, fs, sig_len, record_id = train_dataset[0]\n    print(f'\\nTrain Sample:')\n    print(f'Image shape: {img.shape}')\n    print(f'Signal shape: {signal.shape}')  # Should be (12, STANDARD_LEN)\n    print(f'Sampling frequency: {fs} Hz')\n    print(f'Original signal length: {sig_len}')\n    print(f'Record ID: {record_id}')\n    print(f'\\nNote: All leads interpolated to {STANDARD_LEN} for efficient training')\n    print(f'      Short leads (2500) kept as-is, long leads (10000) downsampled')\nelse:\n    print('No training samples available!')\n\n# Check one sample from val\nif len(val_dataset) > 0:\n    img, signal, fs, sig_len, record_id = val_dataset[0]\n    print(f'\\nVal Sample:')\n    print(f'Image shape: {img.shape}')\n    print(f'Signal shape: {signal.shape}')\n    print(f'Record ID: {record_id}')\nelse:\n    print('No validation samples available!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:37.889607Z","iopub.execute_input":"2025-11-01T06:14:37.889857Z","iopub.status.idle":"2025-11-01T06:14:38.261688Z","shell.execute_reply.started":"2025-11-01T06:14:37.889840Z","shell.execute_reply":"2025-11-01T06:14:38.260893Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize Training Sample\nDisplaying input ECG image (256x256 resized) along with ground truth signals for all 12 leads to verify data quality.","metadata":{}},{"cell_type":"code","source":"# Visualize one training sample - all 12 leads from CSV with input image\nif len(train_dataset) > 0:\n    img, signal, fs, sig_len, record_id = train_dataset[0]\n    \n    print(f'Training Sample Visualization')\n    print(f'Record ID: {record_id}')\n    print(f'Input Image: {record_id}-0001.png (first page)')\n    print(f'Sampling frequency: {fs} Hz')\n    print(f'Original signal length: {sig_len}')\n    print(f'Interpolated to: {STANDARD_LEN}')\n    print(f'Signal shape: {signal.shape} (12 leads, {STANDARD_LEN} samples each)\\n')\n    \n    # Create a figure with input image at top and 12 lead plots below\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    \n    fig = plt.figure(figsize=(12, 12))\n    gs = fig.add_gridspec(13, 1, height_ratios=[10] + [1]*12, hspace=0.3)\n    \n    # Plot the input image at the top\n    ax_img = fig.add_subplot(gs[0])\n    img_display = img.permute(1, 2, 0).numpy()  # Convert from (C, H, W) to (H, W, C)\n    ax_img.imshow(img_display)\n    ax_img.set_title(f'Input ECG Image - Record {record_id}-0001.png', \n                     fontsize=10, fontweight='bold', pad=8)\n    ax_img.axis('off')\n    \n    # Plot all 12 leads\n    for i, lead_name in enumerate(leads):\n        ax = fig.add_subplot(gs[i+1])\n        lead_signal = signal[i].numpy()\n        time_axis = np.arange(len(lead_signal)) / fs  # Convert to seconds\n        \n        ax.plot(time_axis, lead_signal, linewidth=0.8, color='blue')\n        ax.set_ylabel(lead_name, fontweight='bold', fontsize=8)\n        ax.grid(True, alpha=0.3)\n        \n        if i == len(leads) - 1:\n            ax.set_xlabel('Time (seconds)', fontsize=8)\n    \n    plt.suptitle(f'Training Sample - Record {record_id}\\nInput Image (0001) and Ground Truth Signals (all 12 leads)', \n                 fontsize=12, fontweight='bold', y=0.995)\n    plt.tight_layout()\n    plt.show()\n    \n    print(f'\\nSignal statistics:')\n    for i, lead_name in enumerate(leads):\n        lead_signal = signal[i].numpy()\n        print(f'{lead_name:>3}: min={lead_signal.min():7.2f}, max={lead_signal.max():7.2f}, mean={lead_signal.mean():7.2f}, std={lead_signal.std():7.2f}')\nelse:\n    print('No training samples available for visualization!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:38.262694Z","iopub.execute_input":"2025-11-01T06:14:38.262984Z","iopub.status.idle":"2025-11-01T06:14:39.606167Z","shell.execute_reply.started":"2025-11-01T06:14:38.262966Z","shell.execute_reply":"2025-11-01T06:14:39.605487Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Configuration\nSetting up data loaders, model, loss function (MSE), and Adam optimizer with learning rate 0.001.","metadata":{}},{"cell_type":"code","source":"# Training setup\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\nmodel = SimpleCNN(standard_len=STANDARD_LEN, num_leads=12).to(DEVICE)\ncriterion = nn.MSELoss()\noptimizer = optim.Adam(model.parameters(), lr=LR)\n\nprint(f'Model parameters: {sum(p.numel() for p in model.parameters()):,}')\nprint(f'Train batches: {len(train_loader)}')\nprint(f'Val batches: {len(val_loader)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:39.606909Z","iopub.execute_input":"2025-11-01T06:14:39.607125Z","iopub.status.idle":"2025-11-01T06:14:45.400882Z","shell.execute_reply.started":"2025-11-01T06:14:39.607107Z","shell.execute_reply":"2025-11-01T06:14:45.400092Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train the Model","metadata":{}},{"cell_type":"code","source":"# Training loop with validation\nfor epoch in range(EPOCHS):\n    # Training phase\n    model.train()\n    train_loss = 0\n    train_batches = 0\n    \n    for batch_data in train_loader:\n        images, signals, fs_batch, sig_len_batch, record_ids = batch_data\n        images = images.to(DEVICE)\n        signals = signals.to(DEVICE)  # Shape: (batch, 12, standard_len) - all interpolated to same length\n        \n        # Forward pass\n        optimizer.zero_grad()\n        outputs = model(images)  # Shape: (batch, 12, standard_len)\n        \n        # Calculate loss\n        loss = criterion(outputs, signals)\n        \n        # Backward pass\n        loss.backward()\n        optimizer.step()\n        \n        train_loss += loss.item()\n        train_batches += 1\n    \n    avg_train_loss = train_loss / train_batches if train_batches > 0 else 0\n    \n    # Validation phase\n    model.eval()\n    val_loss = 0\n    val_batches = 0\n    \n    with torch.no_grad():\n        for batch_data in val_loader:\n            images, signals, fs_batch, sig_len_batch, record_ids = batch_data\n            images = images.to(DEVICE)\n            signals = signals.to(DEVICE)\n            \n            # Forward pass\n            outputs = model(images)\n            \n            # Calculate loss\n            loss = criterion(outputs, signals)\n            val_loss += loss.item()\n            val_batches += 1\n    \n    avg_val_loss = val_loss / val_batches if val_batches > 0 else 0\n    \n    print(f'Epoch {epoch+1}/{EPOCHS}, Train Loss: {avg_train_loss:.6f}, Val Loss: {avg_val_loss:.6f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:14:45.401550Z","iopub.execute_input":"2025-11-01T06:14:45.401973Z","iopub.status.idle":"2025-11-01T06:17:08.884347Z","shell.execute_reply.started":"2025-11-01T06:14:45.401953Z","shell.execute_reply":"2025-11-01T06:17:08.883739Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare Test Dataset\nCreating test dataset from unique record IDs (test.csv has one row per lead). Model outputs standard length which will be interpolated to required lengths.","metadata":{}},{"cell_type":"code","source":"# Create test dataset and predict\n# Get unique record IDs from test_df (test_df has one row per lead, not per record)\nunique_test_ids = test_df['id'].unique()\ntest_df_unique = pd.DataFrame({'id': unique_test_ids})\n\n# Filter to available test images\ntest_df_unique = test_df_unique[test_df_unique['id'].isin(available_test_ids)].reset_index(drop=True)\n\nprint(f'Unique test records: {len(test_df_unique)}')\ntest_dataset = ECGDataset(test_df_unique, TEST_DIR, is_train=False, standard_len=STANDARD_LEN)\ntest_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)\n\nprint(f'Generating predictions for {len(test_dataset)} test samples...')\nprint(f'Model will output {STANDARD_LEN} values per lead')\nprint(f'Then interpolate to required lengths per test.csv (flexible for any dimension)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:17:08.885010Z","iopub.execute_input":"2025-11-01T06:17:08.885335Z","iopub.status.idle":"2025-11-01T06:17:08.893027Z","shell.execute_reply.started":"2025-11-01T06:17:08.885314Z","shell.execute_reply":"2025-11-01T06:17:08.892434Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Generate Predictions","metadata":{}},{"cell_type":"code","source":"# Generate predictions\nmodel.eval()\npredictions = {}\n\nwith torch.no_grad():\n    for images, record_ids in tqdm(test_loader):\n        images = images.to(DEVICE)\n        outputs = model(images)  # Shape: (batch, 12, standard_len)\n        \n        # Store predictions for each record\n        for i, record_id in enumerate(record_ids):\n            pred_signal = outputs[i].cpu().numpy()  # Shape: (12, standard_len)\n            predictions[record_id] = pred_signal\n\nprint(f'Generated predictions for {len(predictions)} records')\nprint(f'Each prediction has shape (12, {STANDARD_LEN})')\nprint(f'Will be interpolated to required dimensions during submission generation')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:17:08.893738Z","iopub.execute_input":"2025-11-01T06:17:08.894026Z","iopub.status.idle":"2025-11-01T06:17:09.177970Z","shell.execute_reply.started":"2025-11-01T06:17:08.894010Z","shell.execute_reply":"2025-11-01T06:17:09.177378Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Compare Ground Truth vs Prediction\nVisualizing one validation sample showing how well the model predictions match the actual ECG signals across all 12 leads.","metadata":{}},{"cell_type":"code","source":"# Validation sample comparison: Ground Truth vs Prediction (all 12 leads)\nif len(val_dataset) > 0:\n    # Get one validation sample\n    img, gt_signal, fs, sig_len, record_id = val_dataset[0]\n    \n    # Get prediction for this sample\n    model.eval()\n    with torch.no_grad():\n        img_batch = img.unsqueeze(0).to(DEVICE)  # Add batch dimension\n        pred_signal = model(img_batch).cpu().squeeze(0)  # Remove batch dimension\n    \n    print(f'Validation Sample Comparison - GT vs Prediction')\n    print(f'Record ID: {record_id}')\n    print(f'Sampling frequency: {fs} Hz')\n    print(f'Signal length: {STANDARD_LEN}')\n    print(f'GT shape: {gt_signal.shape}')\n    print(f'Prediction shape: {pred_signal.shape}\\n')\n    \n    # Create a figure with 12 subplots (one per lead)\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    \n    fig, axes = plt.subplots(12, 1, figsize=(18, 22))\n    fig.suptitle(f'Validation Sample Comparison - Record {record_id}\\\\nGround Truth (Blue) vs Prediction (Red)', \n                 fontsize=16, fontweight='bold')\n    \n    for i, (ax, lead_name) in enumerate(zip(axes, leads)):\n        gt_lead = gt_signal[i].numpy()\n        pred_lead = pred_signal[i].numpy()\n        time_axis = np.arange(len(gt_lead)) / fs  # Convert to seconds\n        \n        # Plot both signals with different colors and some transparency\n        ax.plot(time_axis, gt_lead, linewidth=1.2, color='blue', alpha=0.7, label='Ground Truth')\n        ax.plot(time_axis, pred_lead, linewidth=1.0, color='red', alpha=0.6, label='Prediction')\n        \n        ax.set_ylabel(lead_name, fontweight='bold', fontsize=12)\n        ax.grid(True, alpha=0.3)\n        \n        # Add legend only to first subplot\n        if i == 0:\n            ax.legend(loc='upper right')\n        \n        if i == len(axes) - 1:\n            ax.set_xlabel('Time (seconds)', fontsize=12)\n    \n    plt.tight_layout()\n    plt.show()\n    \n    # Calculate simple MSE for each lead\n    print(f'\\\\nMean Squared Error (MSE) per lead:')\n    for i, lead_name in enumerate(leads):\n        gt_lead = gt_signal[i].numpy()\n        pred_lead = pred_signal[i].numpy()\n        mse = np.mean((gt_lead - pred_lead) ** 2)\n        print(f'{lead_name:>3}: MSE = {mse:.6f}')\nelse:\n    print('No validation samples available for comparison!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:17:09.178619Z","iopub.execute_input":"2025-11-01T06:17:09.178922Z","iopub.status.idle":"2025-11-01T06:17:10.772827Z","shell.execute_reply.started":"2025-11-01T06:17:09.178904Z","shell.execute_reply":"2025-11-01T06:17:10.772188Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Calculate SNR Metrics\nComputing Signal-to-Noise Ratio (SNR) for train and validation sets to measure prediction quality. Higher SNR means better signal extraction.","metadata":{}},{"cell_type":"code","source":"# Calculate SNR metrics for Train and Validation sets\n# Reference: https://www.kaggle.com/code/metric/physionet-ecg-signal-extraction-metric/\n# SNR (dB) = 10 * log10(P_signal / P_noise)\n# where P_signal is power of original signal, P_noise is power of (original - predicted)\n\ndef calculate_snr(ground_truth, prediction):\n    \"\"\"\n    Calculate Signal-to-Noise Ratio in dB.\n    \n    Args:\n        ground_truth: numpy array of ground truth signal\n        prediction: numpy array of predicted signal\n    \n    Returns:\n        SNR in dB. Returns infinity if prediction is perfect.\n    \"\"\"\n    # Calculate signal power\n    signal_power = np.mean(ground_truth ** 2)\n    \n    # Calculate noise power (mean squared error)\n    noise_power = np.mean((ground_truth - prediction) ** 2)\n    \n    # Avoid division by zero\n    if noise_power < 1e-10:  # Essentially perfect prediction\n        return float('inf')\n    \n    # Calculate SNR in dB\n    snr_db = 10 * np.log10(signal_power / noise_power)\n    \n    return snr_db\n\n\ndef evaluate_snr_on_dataset(dataset, model, device, dataset_name=\"Dataset\"):\n    \"\"\"\n    Evaluate SNR for all samples in a dataset.\n    \n    Args:\n        dataset: ECGDataset instance\n        model: trained model\n        device: torch device\n        dataset_name: name for printing (e.g., \"Train\" or \"Validation\")\n    \n    Returns:\n        Dictionary with SNR statistics\n    \"\"\"\n    model.eval()\n    \n    all_snr = []  # Overall SNR per sample (averaged across all leads)\n    lead_snr = {lead: [] for lead in ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']}\n    leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    \n    print(f'\\\\nEvaluating SNR on {dataset_name} set ({len(dataset)} samples)...')\n    \n    with torch.no_grad():\n        for idx in tqdm(range(len(dataset)), desc=f\"{dataset_name} SNR\"):\n            # Get ground truth\n            img, gt_signal, fs, sig_len, record_id = dataset[idx]\n            \n            # Get prediction\n            img_batch = img.unsqueeze(0).to(device)\n            pred_signal = model(img_batch).cpu().squeeze(0)\n            \n            # Convert to numpy\n            gt_signal = gt_signal.numpy()\n            pred_signal = pred_signal.numpy()\n            \n            # Calculate SNR for each lead\n            sample_snr_list = []\n            for lead_idx, lead_name in enumerate(leads):\n                gt_lead = gt_signal[lead_idx]\n                pred_lead = pred_signal[lead_idx]\n                \n                snr = calculate_snr(gt_lead, pred_lead)\n                lead_snr[lead_name].append(snr)\n                sample_snr_list.append(snr)\n            \n            # Average SNR across all leads for this sample\n            # Filter out infinities for averaging\n            finite_snrs = [s for s in sample_snr_list if not np.isinf(s)]\n            if len(finite_snrs) > 0:\n                avg_snr = np.mean(finite_snrs)\n            else:\n                avg_snr = float('inf')\n            all_snr.append(avg_snr)\n    \n    # Calculate statistics\n    # Filter infinities for statistics\n    finite_all_snr = [s for s in all_snr if not np.isinf(s)]\n    \n    results = {\n        'mean_snr': np.mean(finite_all_snr) if len(finite_all_snr) > 0 else float('inf'),\n        'median_snr': np.median(finite_all_snr) if len(finite_all_snr) > 0 else float('inf'),\n        'std_snr': np.std(finite_all_snr) if len(finite_all_snr) > 0 else 0,\n        'min_snr': np.min(finite_all_snr) if len(finite_all_snr) > 0 else float('inf'),\n        'max_snr': np.max(finite_all_snr) if len(finite_all_snr) > 0 else float('inf'),\n        'lead_snr': {}\n    }\n    \n    # Calculate per-lead statistics\n    for lead_name, snr_list in lead_snr.items():\n        finite_lead_snr = [s for s in snr_list if not np.isinf(s)]\n        if len(finite_lead_snr) > 0:\n            results['lead_snr'][lead_name] = {\n                'mean': np.mean(finite_lead_snr),\n                'median': np.median(finite_lead_snr),\n                'std': np.std(finite_lead_snr)\n            }\n        else:\n            results['lead_snr'][lead_name] = {\n                'mean': float('inf'),\n                'median': float('inf'),\n                'std': 0\n            }\n    \n    return results\n\n\n# Evaluate on Train set\ntrain_results = evaluate_snr_on_dataset(train_dataset, model, DEVICE, \"Train\")\n\nprint(f'\\\\n{\"=\"*60}')\nprint(f'TRAIN SET SNR METRICS')\nprint(f'{\"=\"*60}')\nprint(f'Overall Statistics:')\nprint(f'  Mean SNR:   {train_results[\"mean_snr\"]:.2f} dB')\nprint(f'\\\\nPer-Lead Mean SNR:')\nfor lead_name in ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']:\n    mean_snr = train_results['lead_snr'][lead_name]['mean']\n    print(f'  {lead_name:>3}: {mean_snr:7.2f} dB')\n\n# Evaluate on Validation set\nval_results = evaluate_snr_on_dataset(val_dataset, model, DEVICE, \"Validation\")\n\nprint(f'\\\\n{\"=\"*60}')\nprint(f'VALIDATION SET SNR METRICS')\nprint(f'{\"=\"*60}')\nprint(f'  Mean SNR:   {val_results[\"mean_snr\"]:.2f} dB')\nprint(f'\\\\nPer-Lead Mean SNR:')\nfor lead_name in ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']:\n    mean_snr = val_results['lead_snr'][lead_name]['mean']\n    print(f'  {lead_name:>3}: {mean_snr:7.2f} dB')\n\nprint(f'\\\\n{\"=\"*60}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:17:10.773689Z","iopub.execute_input":"2025-11-01T06:17:10.773990Z","iopub.status.idle":"2025-11-01T06:18:55.274908Z","shell.execute_reply.started":"2025-11-01T06:17:10.773973Z","shell.execute_reply":"2025-11-01T06:18:55.274133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create Submission File","metadata":{}},{"cell_type":"code","source":"# Create submission dataframe with flexible interpolation\ndef interpolate_signal(signal, target_length):\n    \"\"\"Interpolate signal to target length using linear interpolation\"\"\"\n    if len(signal) == target_length:\n        return signal\n    \n    # Create indices for interpolation\n    old_indices = np.linspace(0, len(signal) - 1, len(signal))\n    new_indices = np.linspace(0, len(signal) - 1, target_length)\n    \n    # Linear interpolation\n    interpolated = np.interp(new_indices, old_indices, signal)\n    return interpolated\n\nsubmission_rows = []\nleads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n\nfor idx, row in tqdm(test_df.iterrows(), total=len(test_df)):\n    record_id = str(row['id'])\n    lead_name = row['lead']\n    num_rows = int(row['number_of_rows'])\n    \n    # Skip if we don't have prediction for this record\n    if record_id not in predictions:\n        continue\n    \n    # Get prediction for this record: shape (12, STANDARD_LEN)\n    pred_signal = predictions[record_id]\n    \n    # Get index for this lead\n    lead_idx = leads.index(lead_name)\n    \n    # Extract signal for this lead (length = STANDARD_LEN, typically 2500)\n    lead_signal = pred_signal[lead_idx]  # Shape: (STANDARD_LEN,)\n    \n    # Interpolate from STANDARD_LEN to required num_rows (flexible for any dimension)\n    if len(lead_signal) != num_rows:\n        lead_values = interpolate_signal(lead_signal, num_rows)\n    else:\n        lead_values = lead_signal\n    \n    # Create submission rows\n    for timestep, value in enumerate(lead_values):\n        submission_id = f'{record_id}_{timestep}_{lead_name}'\n        submission_rows.append({'id': submission_id, 'value': float(value)})\n\nsubmission_df = pd.DataFrame(submission_rows)\nprint(f'Submission shape: {submission_df.shape}')\nprint(f'\\nFlexible interpolation summary:')\nprint(f'- All predictions generated at standard length: {STANDARD_LEN}')\nprint(f'- Interpolated to required dimensions per test.csv:')\nprint(f'  - Lead II: {STANDARD_LEN} → 10000 (upsampled 4x)')\nprint(f'  - Other leads: {STANDARD_LEN} → 2500 (kept as-is)')\nprint(f'- This approach works for ANY dimension specified in test.csv')\nsubmission_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:18:55.277092Z","iopub.execute_input":"2025-11-01T06:18:55.277346Z","iopub.status.idle":"2025-11-01T06:18:55.380129Z","shell.execute_reply.started":"2025-11-01T06:18:55.277327Z","shell.execute_reply":"2025-11-01T06:18:55.379381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save submission\nsubmission_df.to_csv('submission.csv', index=False)\nprint('Submission saved to submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:18:55.381023Z","iopub.execute_input":"2025-11-01T06:18:55.381291Z","iopub.status.idle":"2025-11-01T06:18:55.496881Z","shell.execute_reply.started":"2025-11-01T06:18:55.381269Z","shell.execute_reply":"2025-11-01T06:18:55.496338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T06:18:55.497496Z","iopub.execute_input":"2025-11-01T06:18:55.497674Z","iopub.status.idle":"2025-11-01T06:18:55.504970Z","shell.execute_reply.started":"2025-11-01T06:18:55.497659Z","shell.execute_reply":"2025-11-01T06:18:55.504244Z"}},"outputs":[],"execution_count":null}]}