{"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":[],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# PhysioNet ECG Digitization \n\n# ------------------- Imports -------------------------------\nimport os, warnings, random\nwarnings.filterwarnings(\"ignore\")\n\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport matplotlib.pyplot as plt\nfrom scipy.signal import butter, filtfilt\n\n# ------------------- Config -------------------------------\nDATA_PATH = '/kaggle/input/physionet-ecg-image-digitization/'\nWORK_DIR = '/kaggle/working'\nos.makedirs(WORK_DIR, exist_ok=True)\n\nTRAIN_CSV = os.path.join(DATA_PATH, 'train.csv')\nTEST_CSV  = os.path.join(DATA_PATH, 'test.csv')\nTRAIN_DIR = os.path.join(DATA_PATH, 'train')\nSAMPLE_SUB = os.path.join(DATA_PATH, 'sample_submission.parquet')\nSUBMISSION_CSV = os.path.join(WORK_DIR, 'submission.csv')\n\nLEADS = ['I','II','III','aVR','aVL','aVF','V1','V2','V3','V4','V5','V6']\nTEMPLATE_LEN = 500\nMIN_VAL, MAX_VAL = 0.0, 0.07\nSEED = 42\nEPOCHS = 8\nLR = 1e-3\nBATCH_SIZE = 16\n\n# ------------------- Reproducibility -----------------------\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\nseed_everything()\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {device}\")\n\n# ------------------- Load Data ----------------------------\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\nsample_sub = pd.read_parquet(SAMPLE_SUB)\nprint(f\"Train rows: {len(train_df)}, Test rows: {len(test_df)}\")\n\n# ------------------- Dataset ------------------------------\nclass ECGDataset(Dataset):\n    def __init__(self, df, data_dir, leads, mode='train'):\n        self.df = df.reset_index(drop=True)\n        self.data_dir = data_dir\n        self.leads = leads\n        self.mode = mode\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        _id = row['id']\n        signals = []\n        for lead in self.leads:\n            csv_file = os.path.join(self.data_dir, str(_id), f\"{_id}.csv\")\n            if not os.path.exists(csv_file):\n                sig = np.zeros(TEMPLATE_LEN)\n            else:\n                df = pd.read_csv(csv_file)\n                if lead not in df.columns:\n                    sig = np.zeros(TEMPLATE_LEN)\n                else:\n                    sig = df[lead].dropna().values\n                    sig = (sig - np.mean(sig)) / (np.std(sig) + 1e-8)\n                    sig = np.interp(np.linspace(0, 1, TEMPLATE_LEN),\n                                    np.linspace(0, 1, len(sig)), sig)\n            signals.append(sig)\n        x = np.stack(signals, axis=0).astype(np.float32)\n        if self.mode == 'train':\n            return torch.tensor(x), torch.tensor(x)\n        else:\n            return torch.tensor(x)\n\n# ------------------- Model -------------------------------\nclass ECGAutoEncoder(nn.Module):\n    def __init__(self, leads=12):\n        super().__init__()\n        self.encoder = nn.Sequential(\n            nn.Conv1d(leads, 32, 7, padding=3),\n            nn.BatchNorm1d(32),\n            nn.ReLU(),\n            nn.MaxPool1d(2),\n            nn.Conv1d(32, 64, 5, padding=2),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.MaxPool1d(2)\n        )\n        self.decoder = nn.Sequential(\n            nn.ConvTranspose1d(64, 32, 4, stride=2, padding=1),\n            nn.ReLU(),\n            nn.ConvTranspose1d(32, leads, 4, stride=2, padding=1),\n            nn.Tanh()\n        )\n\n    def forward(self, x):\n        x = self.encoder(x)\n        x = self.decoder(x)\n        return x\n\n# ------------------- Data Split --------------------------\nfrom sklearn.model_selection import train_test_split\ntrain_split, val_split = train_test_split(train_df, test_size=0.2, random_state=SEED)\n\ntrain_dataset = ECGDataset(train_split, TRAIN_DIR, LEADS, mode='train')\nval_dataset = ECGDataset(val_split, TRAIN_DIR, LEADS, mode='train')\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\nprint(f\"Train dataset size: {len(train_dataset)} | Val dataset size: {len(val_dataset)}\")\n\n# ------------------- Training ----------------------------\nmodel = ECGAutoEncoder(leads=len(LEADS)).to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=LR)\ncriterion = nn.MSELoss()\n\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss = 0\n    for x, y in train_loader:\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n        output = model(x)\n        loss = criterion(output, y)\n        if not torch.isnan(loss):\n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item()\n    train_loss /= len(train_loader)\n\n    # Validation\n    model.eval()\n    val_loss = 0\n    with torch.no_grad():\n        for x, y in val_loader:\n            x, y = x.to(device), y.to(device)\n            output = model(x)\n            val_loss += criterion(output, y).item()\n    val_loss /= len(val_loader)\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}\")\n\ntorch.save(model.state_dict(), os.path.join(WORK_DIR, \"best_model.pth\"))\n\n# ------------------- Inference ----------------------------\nprint(\"Running inference on test set...\")\ntest_dataset = ECGDataset(test_df, TRAIN_DIR, LEADS, mode='test')\ntest_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)\n\nmodel.eval()\npredictions = {}\n\nfor i, x in enumerate(tqdm(test_loader)):\n    x = x.to(device)\n    with torch.no_grad():\n        y_pred = model(x).cpu().numpy()[0]\n    _id = test_df.iloc[i]['id']\n    for l_idx, lead in enumerate(LEADS):\n        signal = y_pred[l_idx]\n        s_min, s_max = signal.min(), signal.max()\n        signal = (signal - s_min) / (s_max - s_min + 1e-8)\n        signal = MIN_VAL + signal * (MAX_VAL - MIN_VAL)\n        predictions[(_id, lead)] = signal.astype(np.float32)\n\n# ------------------- Build Submission --------------------\nprint(\"Building submission file...\")\nsubmission_data = []\nfor _, row in test_df.iterrows():\n    _id, lead, n_rows = row['id'], row['lead'], row['number_of_rows']\n    signal = predictions[(_id, lead)]\n    signal_resized = np.interp(np.linspace(0, 1, n_rows),\n                               np.linspace(0, 1, TEMPLATE_LEN), signal)\n    for i in range(n_rows):\n        submission_data.append({'id': f\"{_id}_{i}_{lead}\", 'value': float(signal_resized[i])})\n\nsubmission_df = pd.DataFrame(submission_data)\nsubmission_df.to_csv(SUBMISSION_CSV, index=False)\nprint(f\"✅ Submission saved: {SUBMISSION_CSV}\")\n\n# ------------------- Visualization ------------------------\nfig, axs = plt.subplots(4, 3, figsize=(15,10))\nsample_row = test_df.iloc[0]\nfor i, lead in enumerate(LEADS):\n    signal = predictions[(sample_row['id'], lead)]\n    ax = axs[i//3, i%3]\n    ax.plot(signal, color='b')\n    ax.set_title(lead)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T20:21:46.972948Z","iopub.execute_input":"2025-11-01T20:21:46.973325Z","iopub.status.idle":"2025-11-01T20:33:44.558198Z","shell.execute_reply.started":"2025-11-01T20:21:46.973298Z","shell.execute_reply":"2025-11-01T20:33:44.557061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = pd.read_csv('/kaggle/working/submission.csv')\ndisplay(submission_df.head())\nprint(\"Total rows:\", len(submission_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T20:34:41.165763Z","iopub.execute_input":"2025-11-01T20:34:41.166095Z","iopub.status.idle":"2025-11-01T20:34:41.258408Z","shell.execute_reply.started":"2025-11-01T20:34:41.166071Z","shell.execute_reply":"2025-11-01T20:34:41.257203Z"}},"outputs":[],"execution_count":null}]}