{"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":106680,"databundleVersionId":13374319,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"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\nfor 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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom collections import Counter, defaultdict\nfrom tqdm.auto import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nDATA_ROOT = \"/kaggle/input/adaptive-immune-profiling-challenge-2025\"\nTRAIN_ROOT = os.path.join(DATA_ROOT, \"train_datasets\", \"train_datasets\")\nTEST_ROOT  = os.path.join(DATA_ROOT, \"test_datasets\", \"test_datasets\")\n\nTRAIN_DIRS = sorted([os.path.join(TRAIN_ROOT, d) for d in os.listdir(TRAIN_ROOT) if d.startswith(\"train_dataset_\")])\nTEST_DIRS  = sorted([os.path.join(TEST_ROOT,  d) for d in os.listdir(TEST_ROOT)  if d.startswith(\"test_dataset_\")])\n\n# Expanded amino acid vocabulary + padding + unknown\nAA_VOCAB = \"ACDEFGHIKLMNPQRSTVWY-X\"\naa2idx = {aa: i+1 for i, aa in enumerate(AA_VOCAB)}\nVOCAB_SIZE = len(AA_VOCAB) + 1  # +1 for padding_idx=0\n\nMAX_LEN = 30  # Most CDR3/junction_aa are <25, 30 is safe\n\ndef encode_seq(seq: str):\n    if not isinstance(seq, str):\n        seq = \"\"\n    seq = seq.upper()[:MAX_LEN]\n    return [aa2idx.get(aa, 0) for aa in seq] + [0] * (MAX_LEN - len(seq))\n\n# Better gene hashing with larger buckets\ndef hash_gene(gene, buckets=256):\n    if not isinstance(gene, str):\n        return 0\n    return (hash(gene) % buckets) + 1\n\nclass AIRRDataset(Dataset):\n    def __init__(self, folder, max_seqs_per_file=10000, shuffle=True):\n        self.folder = folder\n        self.files = [f for f in os.listdir(folder) if f.endswith(\".tsv\")]\n        self.max_seqs = max_seqs_per_file\n        self.shuffle = shuffle\n\n    def __len__(self):\n        return len(self.files)\n\n    def __getitem__(self, idx):\n        file_path = os.path.join(self.folder, self.files[idx])\n        df = pd.read_csv(file_path, sep=\"\\t\", usecols=[\"junction_aa\", \"v_call\", \"j_call\"])\n        \n        if self.shuffle and len(df) > self.max_seqs:\n            df = df.sample(n=self.max_seqs, random_state=42)\n        elif len(df) > self.max_seqs:\n            df = df.iloc[:self.max_seqs]\n\n        seqs = torch.tensor([encode_seq(s) for s in df[\"junction_aa\"]], dtype=torch.long)\n        vs   = torch.tensor([hash_gene(g) for g in df[\"v_call\"]], dtype=torch.long)\n        js   = torch.tensor([hash_gene(g) for g in df[\"j_call\"]], dtype=torch.long)\n\n        repertoire_id = self.files[idx].replace(\".tsv\", \"\")\n        \n        return seqs, vs, js, repertoire_id\n\n# ==================== ADVANCED MODEL (Improved DeepRC-style) ====================\nclass AdvancedImmuneMIL(nn.Module):\n    def __init__(self, embed_dim=64, hidden_dim=128, dropout=0.3):\n        super().__init__()\n        \n        # AA embedding\n        self.aa_emb = nn.Embedding(VOCAB_SIZE, embed_dim, padding_idx=0)\n        \n        # V/J embeddings (larger buckets)\n        self.v_emb = nn.Embedding(257, 16)  # 0 + 1..256\n        self.j_emb = nn.Embedding(257, 16)\n        \n        # Deeper CNN for better motif capture\n        self.cnn = nn.Sequential(\n            nn.Conv1d(embed_dim, hidden_dim, kernel_size=3, padding=1),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Conv1d(hidden_dim, hidden_dim, kernel_size=5, padding=2),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Conv1d(hidden_dim, hidden_dim, kernel_size=7, padding=3),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.AdaptiveMaxPool1d(1)  # Global max pooling\n        )\n        \n        feat_dim = hidden_dim + 16 + 16\n        \n        # Gated attention (better than simple tanh)\n        self.attention = nn.Sequential(\n            nn.Linear(feat_dim, hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n        # Classifier head\n        self.classifier = nn.Sequential(\n            nn.Linear(feat_dim, hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 1)\n        )\n\n    def forward(self, seqs, vs, js):\n        # seqs: [N, L], vs/js: [N]\n        aa = self.aa_emb(seqs)               # [N, L, embed_dim]\n        aa = aa.permute(0, 2, 1)              # [N, embed_dim, L]\n        cnn_out = self.cnn(aa).squeeze(-1)    # [N, hidden_dim]\n        \n        v_emb = self.v_emb(vs)                # [N, 16]\n        j_emb = self.j_emb(js)                # [N, 16]\n        \n        features = torch.cat([cnn_out, v_emb, j_emb], dim=1)  # [N, feat_dim]\n        \n        att_weights = torch.softmax(self.attention(features), dim=0)  # [N, 1]\n        \n        bag_rep = torch.sum(att_weights * features, dim=0, keepdim=True)  # [1, feat_dim]\n        \n        logit = self.classifier(bag_rep)\n        prob = torch.sigmoid(logit)\n        \n        return prob.squeeze(), att_weights.squeeze()\n\n# ==================== TRAINING & EXTRACTION ====================\nall_positive_probs = []\nall_top_sequences = []\n\nprint(\"🚀 Starting advanced pipeline with cross-training ensemble\")\n\nfor i, train_dir in enumerate(TRAIN_DIRS):\n    print(f\"\\nProcessing train_dataset_{i+1} as positive class\")\n    \n    # Load positive repertoires\n    pos_dataset = AIRRDataset(train_dir, max_seqs_per_file=15000)\n    pos_loader = DataLoader(pos_dataset, batch_size=1, shuffle=False)\n    \n    # Create pseudo-negative from other train datasets (leave-one-out style)\n    neg_dirs = [d for j, d in enumerate(TRAIN_DIRS) if j != i]\n    neg_dataset = torch.utils.data.ConcatDataset([\n        AIRRDataset(neg_dir, max_seqs_per_file=5000) for neg_dir in neg_dirs\n    ])\n    neg_loader = DataLoader(neg_dataset, batch_size=1, shuffle=False)\n    \n    # Train multiple models for ensemble\n    models = []\n    for ens_id in range(5):  # 5-model ensemble\n        model = AdvancedImmuneMIL().to(DEVICE)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-5)\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5)\n        loss_fn = nn.BCELoss()\n        \n        model.train()\n        for epoch in range(8):\n            epoch_losses = []\n            for seqs, vs, js, _ in tqdm(pos_loader + neg_loader, desc=f\"Ensemble {ens_id+1} Epoch {epoch+1}\", leave=False):\n                seqs, vs, js = seqs.squeeze(0).to(DEVICE), vs.squeeze(0).to(DEVICE), js.squeeze(0).to(DEVICE)\n                label = torch.tensor(1.0 if seqs in pos_loader.dataset else 0.0, device=DEVICE)\n                \n                optimizer.zero_grad()\n                prob, _ = model(seqs, vs, js)\n                loss = loss_fn(prob.unsqueeze(0), label.unsqueeze(0))\n                loss.backward()\n                optimizer.step()\n                epoch_losses.append(loss.item())\n            scheduler.step(np.mean(epoch_losses))\n        \n        model.eval()\n        models.append(model)\n    \n    # === Task 1: Predict on matching test datasets ===\n    ds_num = str(i+1)\n    matching_tests = [t for t in TEST_DIRS if f\"_{ds_num}\" in os.path.basename(t) or (len(ds_num)==1 and ds_num in os.path.basename(t))]\n    \n    for test_dir in matching_tests:\n        test_name = os.path.basename(test_dir)\n        test_dataset = AIRRDataset(test_dir, max_seqs_per_file=20000, shuffle=False)\n        test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)\n        \n        reps = []\n        with torch.no_grad():\n            for seqs, vs, js, rep_id in test_loader:\n                seqs, vs, js = seqs.squeeze(0).to(DEVICE), vs.squeeze(0).to(DEVICE), js.squeeze(0).to(DEVICE)\n                \n                probs = [model(seqs, vs, js)[0].item() for model in models]\n                mean_prob = np.mean(probs)\n                \n                all_positive_probs.append({\n                    \"ID\": rep_id,\n                    \"dataset\": test_name,\n                    \"label_positive_probability\": mean_prob\n                })\n                \n                reps.append((seqs.cpu(), vs.cpu(), js.cpu(), rep_id))\n        \n        # === Task 2: Extract top attended sequences from positive train ===\n        with torch.no_grad():\n            top_k_counter = Counter()\n            for seqs, vs, js, _ in pos_loader:\n                seqs, vs, js = seqs.squeeze(0).to(DEVICE), vs.squeeze(0).to(DEVICE), js.squeeze(0).to(DEVICE)\n                \n                ensemble_weights = []\n                for model in models:\n                    _, weights = model(seqs, vs, js)\n                    ensemble_weights.append(weights.cpu().numpy())\n                avg_weights = np.mean(ensemble_weights, axis=0)\n                \n                # Weighted by attention and model confidence (~1.0 for positive)\n                scores = avg_weights * 0.99  # approx confidence\n                \n                # Get original order (assuming df order preserved)\n                # To get actual sequences, we need to reload one file for mapping\n                # Simplified: use high attention as proxy\n                top_indices = np.argsort(-scores)[:500]  # top 500 per repertoire\n                \n                file_path = os.path.join(train_dir, f\"{rep_id}.tsv\")  # rep_id from loader\n                df_top = pd.read_csv(file_path, sep=\"\\t\")[[\"junction_aa\", \"v_call\", \"j_call\"]].iloc[top_indices]\n                for _, row in df_top.iterrows():\n                    key = (row[\"junction_aa\"], row[\"v_call\"], row[\"j_call\"])\n                    top_k_counter[key] += 1  # global frequency boost\n        \n        # Add top sequences globally\n        for rank, (seq_tuple, count) in enumerate(top_k_counter.most_common(50000)):\n            aa, v, j = seq_tuple\n            all_top_sequences.append({\n                \"ID\": f\"train_dataset_{ds_num}_top_{rank+1}\",\n                \"dataset\": f\"train_dataset_{ds_num}\",\n                \"junction_aa\": aa,\n                \"v_call\": v,\n                \"j_call\": j\n            })\n    \n    # Cleanup\n    del models, pos_dataset, neg_dataset\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# ==================== SUBMISSION BUILD ====================\ndf_probs = pd.DataFrame(all_positive_probs)\ndf_seqs  = pd.DataFrame(all_top_sequences)\n\n# Fill placeholders\ndf_probs[[\"junction_aa\", \"v_call\", \"j_call\"]] = \"\"\ndf_seqs[\"label_positive_probability\"] = -1.0  # or np.nan\n\nsubmission = pd.concat([df_probs, df_seqs], ignore_index=True)\nsubmission = submission[[\"ID\", \"dataset\", \"label_positive_probability\", \"junction_aa\", \"v_call\", \"j_call\"]]\nsubmission = submission.fillna(\"\")\n\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(f\"✅ Advanced submission ready! Shape: {submission.shape}\")\nprint(\"   Task 1 entries:\", len(df_probs))\nprint(\"   Task 2 sequences:\", len(df_seqs))\nsubmission.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T19:17:43.648074Z","iopub.execute_input":"2025-12-14T19:17:43.648409Z","iopub.status.idle":"2025-12-14T19:17:56.369926Z","shell.execute_reply.started":"2025-12-14T19:17:43.648381Z","shell.execute_reply":"2025-12-14T19:17:56.367956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport numpy as np\nimport pandas as pd\nfrom collections import Counter\nfrom tqdm.auto import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nDATA_ROOT = \"/kaggle/input/adaptive-immune-profiling-challenge-2025\"\nTRAIN_ROOT = os.path.join(DATA_ROOT, \"train_datasets\", \"train_datasets\")\nTEST_ROOT  = os.path.join(DATA_ROOT, \"test_datasets\", \"test_datasets\")\n\nTRAIN_DIRS = sorted([os.path.join(TRAIN_ROOT, d) for d in os.listdir(TRAIN_ROOT) if d.startswith(\"train_dataset_\")])\nTEST_DIRS  = sorted([os.path.join(TEST_ROOT,  d) for d in os.listdir(TEST_ROOT)  if d.startswith(\"test_dataset_\")])\n\n# Expanded vocabulary\nAA_VOCAB = \"ACDEFGHIKLMNPQRSTVWY-X\"\naa2idx = {aa: i+1 for i, aa in enumerate(AA_VOCAB)}\nVOCAB_SIZE = len(AA_VOCAB) + 1\n\nMAX_LEN = 30\n\ndef encode_seq(seq: str):\n    if not isinstance(seq, str):\n        seq = \"\"\n    seq = seq.upper()[:MAX_LEN]\n    return [aa2idx.get(aa, 0) for aa in seq] + [0] * (MAX_LEN - len(seq))\n\ndef hash_gene(gene, buckets=256):\n    if not isinstance(gene, str):\n        return 0\n    return (hash(gene) % buckets) + 1\n\nclass AIRRDataset(Dataset):\n    def __init__(self, folder, max_seqs_per_file=15000, shuffle=True):\n        self.folder = folder\n        self.files = [f for f in os.listdir(folder) if f.endswith(\".tsv\")]\n        self.max_seqs = max_seqs_per_file\n        self.shuffle = shuffle\n\n    def __len__(self):\n        return len(self.files)\n\n    def __getitem__(self, idx):\n        file_path = os.path.join(self.folder, self.files[idx])\n        df = pd.read_csv(file_path, sep=\"\\t\", usecols=[\"junction_aa\", \"v_call\", \"j_call\"])\n        \n        if len(df) > self.max_seqs:\n            if self.shuffle:\n                df = df.sample(n=self.max_seqs, random_state=42)\n            else:\n                df = df.iloc[:self.max_seqs]\n\n        seqs = torch.tensor([encode_seq(s) for s in df[\"junction_aa\"]], dtype=torch.long)\n        vs   = torch.tensor([hash_gene(g) for g in df[\"v_call\"]], dtype=torch.long)\n        js   = torch.tensor([hash_gene(g) for g in df[\"j_call\"]], dtype=torch.long)\n\n        rep_id = self.files[idx].replace(\".tsv\", \"\")\n        \n        return seqs, vs, js, rep_id\n\nclass AdvancedImmuneMIL(nn.Module):\n    def __init__(self, embed_dim=64, hidden_dim=128, dropout=0.3):\n        super().__init__()\n        \n        self.aa_emb = nn.Embedding(VOCAB_SIZE, embed_dim, padding_idx=0)\n        self.v_emb = nn.Embedding(257, 16)\n        self.j_emb = nn.Embedding(257, 16)\n        \n        self.cnn = nn.Sequential(\n            nn.Conv1d(embed_dim, hidden_dim, 3, padding=1),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Conv1d(hidden_dim, hidden_dim, 5, padding=2),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Conv1d(hidden_dim, hidden_dim, 7, padding=3),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.AdaptiveMaxPool1d(1)\n        )\n        \n        feat_dim = hidden_dim + 32\n        \n        self.attention = nn.Sequential(\n            nn.Linear(feat_dim, hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n        self.classifier = nn.Sequential(\n            nn.Linear(feat_dim, hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 1)\n        )\n\n    def forward(self, seqs, vs, js):\n        aa = self.aa_emb(seqs).permute(0, 2, 1)\n        cnn_out = self.cnn(aa).squeeze(-1)\n        \n        v_emb = self.v_emb(vs)\n        j_emb = self.j_emb(js)\n        \n        features = torch.cat([cnn_out, v_emb, j_emb], dim=1)\n        \n        att_weights = torch.softmax(self.attention(features), dim=0)\n        \n        bag_rep = torch.sum(att_weights * features, dim=0, keepdim=True)\n        \n        prob = torch.sigmoid(self.classifier(bag_rep))\n        \n        return prob.squeeze(), att_weights.squeeze()\n\n# ==================== PIPELINE ====================\nall_positive_probs = []\nall_top_sequences = []\n\nprint(\"🚀 Running final advanced pipeline\")\n\nfor i, train_dir in enumerate(TRAIN_DIRS):\n    ds_num = str(i + 1)\n    print(f\"\\n=== Processing train_dataset_{ds_num} ===\")\n    \n    pos_dataset = AIRRDataset(train_dir, max_seqs_per_file=15000)\n    pos_loader = DataLoader(pos_dataset, batch_size=1, shuffle=False)\n    \n    neg_dirs = [d for j, d in enumerate(TRAIN_DIRS) if j != i]\n    neg_samples = []\n    for neg_dir in neg_dirs:\n        neg_ds = AIRRDataset(neg_dir, max_seqs_per_file=3000, shuffle=True)\n        neg_samples.extend(list(DataLoader(neg_ds, batch_size=1, shuffle=True)))\n    \n    # Train 4 models for ensemble\n    models = []\n    for ens in range(4):\n        model = AdvancedImmuneMIL().to(DEVICE)\n        opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-5)\n        loss_fn = nn.BCELoss()\n        \n        model.train()\n        for epoch in range(7):\n            total_loss = 0\n            batches = list(pos_loader) + neg_samples[:len(pos_loader)]  # balanced\n            for seqs, vs, js, _ in tqdm(batches, desc=f\"Model {ens+1} Epoch {epoch+1}\", leave=False):\n                seqs = seqs.squeeze(0).to(DEVICE)\n                vs = vs.squeeze(0).to(DEVICE)\n                js = js.squeeze(0).to(DEVICE)\n                label = 1.0 if seqs in [b[0] for b in pos_loader] else 0.0\n                label = torch.tensor([label], device=DEVICE)\n                \n                opt.zero_grad()\n                prob, _ = model(seqs, vs, js)\n                loss = loss_fn(prob.unsqueeze(0), label)\n                loss.backward()\n                opt.step()\n                total_loss += loss.item()\n        \n        model.eval()\n        models.append(model)\n    \n    # Task 1: Predict on matching test sets\n    matching_tests = [t for t in TEST_DIRS if ds_num in os.path.basename(t)]\n    for test_dir in matching_tests:\n        test_name = os.path.basename(test_dir)\n        test_ds = AIRRDataset(test_dir, max_seqs_per_file=20000, shuffle=False)\n        test_loader = DataLoader(test_ds, batch_size=1)\n        \n        with torch.no_grad():\n            for seqs, vs, js, rep_id in test_loader:\n                seqs, vs, js = seqs.squeeze(0).to(DEVICE), vs.squeeze(0).to(DEVICE), js.squeeze(0).to(DEVICE)\n                probs = [m(seqs, vs, js)[0].item() for m in models]\n                mean_prob = np.mean(probs)\n                all_positive_probs.append({\n                    \"ID\": rep_id,\n                    \"dataset\": test_name,\n                    \"label_positive_probability\": mean_prob\n                })\n    \n    # Task 2: Extract top sequences from positive train\n    top_counter = Counter()\n    with torch.no_grad():\n        for seqs, vs, js, rep_id in pos_loader:\n            seqs, vs, js = seqs.squeeze(0).to(DEVICE), vs.squeeze(0).to(DEVICE), js.squeeze(0).to(DEVICE)\n            \n            all_weights = []\n            for model in models:\n                _, w = model(seqs, vs, js)\n                all_weights.append(w.cpu().numpy())\n            avg_att = np.mean(all_weights, axis=0)\n            \n            top_idx = np.argsort(-avg_att)[:800]\n            \n            file_path = os.path.join(train_dir, f\"{rep_id}.tsv\")\n            df = pd.read_csv(file_path, sep=\"\\t\", usecols=[\"junction_aa\", \"v_call\", \"j_call\"]).iloc[top_idx]\n            \n            for _, row in df.iterrows():\n                key = (row[\"junction_aa\"], row[\"v_call\"], row[\"j_call\"])\n                top_counter[key] += avg_att[top_idx[df.index.get_loc(_)]]\n\n    for rank, (seq_tuple, _) in enumerate(top_counter.most_common(60000)):\n        aa, v, j = seq_tuple\n        all_top_sequences.append({\n            \"ID\": f\"topseq_{ds_num}_{rank+1}\",\n            \"dataset\": f\"train_dataset_{ds_num}\",\n            \"junction_aa\": aa,\n            \"v_call\": v,\n            \"j_call\": j\n        })\n    \n    del models\n    torch.cuda.empty_cache()\n    gc.collect()\n\n# ==================== SUBMISSION ====================\ndf_probs = pd.DataFrame(all_positive_probs)\ndf_seqs = pd.DataFrame(all_top_sequences)\n\ndf_probs[[\"junction_aa\", \"v_call\", \"j_call\"]] = \"\"\ndf_seqs[\"label_positive_probability\"] = -1.0\n\nsubmission = pd.concat([df_probs, df_seqs]).reset_index(drop=True)\nsubmission = submission[[\"ID\", \"dataset\", \"label_positive_probability\", \"junction_aa\", \"v_call\", \"j_call\"]]\nsubmission.fillna(\"\", inplace=True)\n\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\n✅ Final submission created!\")\nprint(f\"Task 1 rows: {len(df_probs)}\")\nprint(f\"Task 2 sequences: {len(df_seqs)}\")\nprint(f\"Total rows: {len(submission)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-14T19:19:29.670831Z","iopub.execute_input":"2025-12-14T19:19:29.671173Z","iopub.status.idle":"2025-12-14T19:28:08.173552Z","shell.execute_reply.started":"2025-12-14T19:19:29.671148Z","shell.execute_reply":"2025-12-14T19:28:08.172013Z"}},"outputs":[],"execution_count":null}]}