{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"1a47439f","cell_type":"markdown","source":"# Phase 4 — Branch C: Pretrained Sequence Embedding Model (TCR-SSL / ESM-2)\n\nAdds per-sequence pretrained embeddings (TCR-specific SSL or generic PLM like ESM-2), abundance-weighted mean pooling, top-clone pooling, PCA/UMAP diagnostics, and a shallow classifier on pooled embeddings. Outputs branch_c_oof.csv.\n\n---\n\n## Kaggle inputs to add before running this notebook\n\nAdd the following as Kaggle inputs (via the \"Add Input\" button on the right\npanel of the notebook editor):\n- AIRR-ML competition dataset\n- Phase 1 notebook output\n\n## Workflow\n\n1. Run the **SETUP** cell — it creates `/kaggle/working/project/` and auto-merges\n   any previous-phase notebook outputs found under `/kaggle/input/`.\n2. Run each subsequent cell in order. The **Run** cell executes the phase's\n   training script; the **Inspect** cell prints a quick summary of the outputs.\n3. When the run completes, click **Save Version → Save & Run All (Commit)** so\n   the next phase can pick this phase's outputs up via \"Add Input\".\n","metadata":{}},{"id":"49d9f410","cell_type":"code","source":"# ============================================================\n# SETUP — Initialize project + merge previous-phase inputs\n# ============================================================\n# This cell:\n#   1. Creates /kaggle/working/project/ fresh (idempotent re-runs).\n#   2. Auto-detects previous-phase notebook outputs under /kaggle/input/.\n#   3. Merges their project/ contents (src/, artifacts/, configs/) into\n#      /kaggle/working/project/ so this phase can build on them.\n#\n# Kaggle \"Add Input\" workflow:\n#   - Phase 1: add the AIRR-ML competition dataset only.\n#   - Phase N (N>=2): add the AIRR-ML competition dataset AND the previous\n#     phase notebook output(s). For Phase 9 add ALL of Phases 1..8.\n# ============================================================\n\nimport os\nimport shutil\nfrom pathlib import Path\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\n# Reset working project dir (idempotent re-runs)\nif PROJECT_ROOT.exists():\n    shutil.rmtree(PROJECT_ROOT)\nPROJECT_ROOT.mkdir(parents=True, exist_ok=True)\n(PROJECT_ROOT / \"src\").mkdir(parents=True, exist_ok=True)\n(PROJECT_ROOT / \"src\" / \"__init__.py\").write_text(\"\", encoding=\"utf-8\")\n(PROJECT_ROOT / \"configs\").mkdir(parents=True, exist_ok=True)\n(PROJECT_ROOT / \"artifacts\").mkdir(parents=True, exist_ok=True)\n\n# Auto-detect and merge previous-phase inputs.\n# Each previous-phase notebook output should contain a top-level `project/`\n# directory (created by Phase 1 and propagated through every later phase).\nprev_inputs_found = []\ninput_root = Path(\"/kaggle/input\")\nif input_root.exists():\n    for entry in sorted(input_root.iterdir()):\n        if not entry.is_dir():\n            continue\n        prev_project = entry / \"project\"\n        if not prev_project.is_dir():\n            continue\n        prev_inputs_found.append(entry.name)\n        for sub in [\"src\", \"artifacts\", \"configs\"]:\n            src_dir = prev_project / sub\n            if not src_dir.exists():\n                continue\n            for path in src_dir.rglob(\"*\"):\n                if path.is_file():\n                    rel = path.relative_to(src_dir)\n                    target = PROJECT_ROOT / sub / rel\n                    target.parent.mkdir(parents=True, exist_ok=True)\n                    shutil.copy2(path, target)\n\nprint(\"PROJECT_ROOT :\", PROJECT_ROOT)\nif prev_inputs_found:\n    print(\"Previous-phase inputs merged:\")\n    for name in prev_inputs_found:\n        print(\"  -\", name)\nelse:\n    print(\"Previous-phase inputs: (none — running fresh)\")\nprint(\"\\nExisting artifacts:\")\nart_dir = PROJECT_ROOT / \"artifacts\"\nif art_dir.exists():\n    for p in sorted(art_dir.glob(\"*\")):\n        print(\"  -\", p.name)\nelse:\n    print(\"  (none)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:05:58.268694Z","iopub.execute_input":"2026-07-24T11:05:58.269011Z","iopub.status.idle":"2026-07-24T11:05:58.286408Z","shell.execute_reply.started":"2026-07-24T11:05:58.268975Z","shell.execute_reply":"2026-07-24T11:05:58.285487Z"}},"outputs":[],"execution_count":null},{"id":"8b7b4396","cell_type":"code","source":"# Cell 1 — branch_c.yaml write করো\nfrom pathlib import Path\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nbranch_c_yaml = \"\"\"\nenabled: true\n\npaths:\n  output_root: /kaggle/working/project/artifacts/phase4_branch_c\n\nmodel:\n  route: auto\n  local_model_dir: \"\"\n  batch_size: 16\n  max_length: 64\n  fallback_dim: 128\n\nembedding:\n  max_sequences_per_file: 20000\n  top_k_pool: 64\n\ntraining:\n  seeds: [42, 52, 62]\n\"\"\"\n\n(PROJECT_ROOT / \"configs\" / \"branch_c.yaml\").write_text(branch_c_yaml.strip() + \"\\n\", encoding=\"utf-8\")\nprint(\"Written:\", PROJECT_ROOT / \"configs\" / \"branch_c.yaml\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:05:58.288197Z","iopub.execute_input":"2026-07-24T11:05:58.288490Z","iopub.status.idle":"2026-07-24T11:05:58.301347Z","shell.execute_reply.started":"2026-07-24T11:05:58.288457Z","shell.execute_reply":"2026-07-24T11:05:58.300463Z"}},"outputs":[],"execution_count":null},{"id":"daacad72","cell_type":"code","source":"# Cell 2 — src/branch_c_embeddings.py আর train_branch_c.py write করো\nfrom pathlib import Path\nfrom textwrap import dedent\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nbranch_c_embeddings_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nimport math\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Tuple\n\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nfrom .branch_a_features import read_repertoire\nfrom .utils import stable_hash\n\nAA_PROPERTIES = {\n    \"A\": {\"hydro\": 1.8, \"vol\": 88.6, \"charge\": 0.0, \"polar\": 0},\n    \"R\": {\"hydro\": -4.5, \"vol\": 173.4, \"charge\": 1.0, \"polar\": 1},\n    \"N\": {\"hydro\": -3.5, \"vol\": 114.1, \"charge\": 0.0, \"polar\": 1},\n    \"D\": {\"hydro\": -3.5, \"vol\": 111.1, \"charge\": -1.0, \"polar\": 1},\n    \"C\": {\"hydro\": 2.5, \"vol\": 108.5, \"charge\": 0.0, \"polar\": 0},\n    \"Q\": {\"hydro\": -3.5, \"vol\": 143.8, \"charge\": 0.0, \"polar\": 1},\n    \"E\": {\"hydro\": -3.5, \"vol\": 138.4, \"charge\": -1.0, \"polar\": 1},\n    \"G\": {\"hydro\": -0.4, \"vol\": 60.1, \"charge\": 0.0, \"polar\": 0},\n    \"H\": {\"hydro\": -3.2, \"vol\": 153.2, \"charge\": 0.5, \"polar\": 1},\n    \"I\": {\"hydro\": 4.5, \"vol\": 166.7, \"charge\": 0.0, \"polar\": 0},\n    \"L\": {\"hydro\": 3.8, \"vol\": 166.7, \"charge\": 0.0, \"polar\": 0},\n    \"K\": {\"hydro\": -3.9, \"vol\": 168.6, \"charge\": 1.0, \"polar\": 1},\n    \"M\": {\"hydro\": 1.9, \"vol\": 162.9, \"charge\": 0.0, \"polar\": 0},\n    \"F\": {\"hydro\": 2.8, \"vol\": 189.9, \"charge\": 0.0, \"polar\": 0},\n    \"P\": {\"hydro\": -1.6, \"vol\": 112.7, \"charge\": 0.0, \"polar\": 0},\n    \"S\": {\"hydro\": -0.8, \"vol\": 89.0, \"charge\": 0.0, \"polar\": 1},\n    \"T\": {\"hydro\": -0.7, \"vol\": 116.1, \"charge\": 0.0, \"polar\": 1},\n    \"W\": {\"hydro\": -0.9, \"vol\": 227.8, \"charge\": 0.0, \"polar\": 0},\n    \"Y\": {\"hydro\": -1.3, \"vol\": 193.6, \"charge\": 0.0, \"polar\": 1},\n    \"V\": {\"hydro\": 4.2, \"vol\": 140.0, \"charge\": 0.0, \"polar\": 0},\n}\nAA_LIST = list(\"ARNDCQEGHILKMFPSTWYV\")\nAA_INDEX = {aa: i for i, aa in enumerate(AA_LIST)}\nAROMATIC = set([\"F\", \"W\", \"Y\"])\nHYDROPHOBIC = set([\"A\", \"V\", \"I\", \"L\", \"M\", \"F\", \"W\", \"Y\", \"C\"])\nPOLAR = set([aa for aa, p in AA_PROPERTIES.items() if p[\"polar\"] == 1])\n\n\ndef softmax_np(x):\n    x = np.asarray(x, dtype=float)\n    if x.ndim == 0:\n        return np.array([1.0], dtype=float)\n    z = x - np.max(x)\n    ez = np.exp(z)\n    s = ez.sum()\n    if s <= 0:\n        return np.ones_like(ez) / max(1, len(ez))\n    return ez / s\n\n\ndef aggregate_unique_sequences(df: pd.DataFrame) -> pd.DataFrame:\n    if df is None or len(df) == 0:\n        return pd.DataFrame(columns=[\"junction_aa\", \"templates\", \"v_call\", \"j_call\"])\n\n    x = df.copy()\n    x[\"junction_aa\"] = x[\"junction_aa\"].fillna(\"\").astype(str)\n    x[\"templates\"] = pd.to_numeric(x[\"templates\"], errors=\"coerce\").fillna(1.0).clip(lower=1.0)\n\n    if \"v_call\" not in x.columns:\n        x[\"v_call\"] = \"\"\n    if \"j_call\" not in x.columns:\n        x[\"j_call\"] = \"\"\n\n    x = x[x[\"junction_aa\"] != \"\"].copy()\n    if len(x) == 0:\n        return pd.DataFrame(columns=[\"junction_aa\", \"templates\", \"v_call\", \"j_call\"])\n\n    grouped = (\n        x.groupby(\"junction_aa\", as_index=False)\n         .agg({\n             \"templates\": \"sum\",\n             \"v_call\": \"first\",\n             \"j_call\": \"first\",\n         })\n         .sort_values(\"templates\", ascending=False)\n         .reset_index(drop=True)\n    )\n    return grouped\n\n\nclass FallbackEmbedder:\n    def __init__(self, dim: int = 128):\n        self.dim = int(dim)\n        self.name = f\"fallback_{self.dim}d\"\n        self.base_hash_dim = 96\n        if self.dim != 128:\n            self.base_hash_dim = max(32, self.dim - 32)\n\n    def _hash_kmer(self, kmer: str, mod: int):\n        return stable_hash(kmer, mod=mod)\n\n    def embed_one(self, seq: str, v_call: str = \"\", j_call: str = \"\") -> np.ndarray:\n        vec_parts = []\n\n        # 20 AA composition\n        aa_vec = np.zeros(20, dtype=np.float32)\n        valid = 0\n        hydro = vol = charge = charge_abs = 0.0\n        aromatic = hydrophobic = polar = 0.0\n\n        for aa in seq:\n            if aa in AA_INDEX:\n                aa_vec[AA_INDEX[aa]] += 1.0\n                valid += 1\n                hydro += AA_PROPERTIES[aa][\"hydro\"]\n                vol += AA_PROPERTIES[aa][\"vol\"]\n                charge += AA_PROPERTIES[aa][\"charge\"]\n                charge_abs += abs(AA_PROPERTIES[aa][\"charge\"])\n                aromatic += float(aa in AROMATIC)\n                hydrophobic += float(aa in HYDROPHOBIC)\n                polar += float(aa in POLAR)\n\n        if valid > 0:\n            aa_vec /= valid\n            hydro /= valid\n            vol /= valid\n            charge /= valid\n            charge_abs /= valid\n            aromatic /= valid\n            hydrophobic /= valid\n            polar /= valid\n\n        vec_parts.append(aa_vec.astype(np.float32))\n\n        # 96 hashed 3-mer vector\n        hdim = self.base_hash_dim\n        hash_vec = np.zeros(hdim, dtype=np.float32)\n        if len(seq) >= 3:\n            total_k = 0\n            for i in range(len(seq) - 3 + 1):\n                km = seq[i:i+3]\n                idx = self._hash_kmer(km, mod=hdim)\n                hash_vec[idx] += 1.0\n                total_k += 1\n            if total_k > 0:\n                hash_vec /= total_k\n        vec_parts.append(hash_vec)\n\n        # 8 physicochemical / shape features\n        phys = np.array([\n            min(len(seq), 40) / 40.0,\n            hydro,\n            vol / 250.0,\n            charge,\n            charge_abs,\n            aromatic,\n            hydrophobic,\n            polar,\n        ], dtype=np.float32)\n        vec_parts.append(phys)\n\n        # 4 gene hashes\n        gene = np.zeros(4, dtype=np.float32)\n        if v_call:\n            gene[0] = (stable_hash(v_call + \"|0\", mod=10007) % 1000) / 1000.0\n            gene[1] = (stable_hash(v_call + \"|1\", mod=10007) % 1000) / 1000.0\n        if j_call:\n            gene[2] = (stable_hash(j_call + \"|0\", mod=10007) % 1000) / 1000.0\n            gene[3] = (stable_hash(j_call + \"|1\", mod=10007) % 1000) / 1000.0\n        vec_parts.append(gene.astype(np.float32))\n\n        out = np.concatenate(vec_parts).astype(np.float32)\n\n        if len(out) < self.dim:\n            pad = np.zeros(self.dim - len(out), dtype=np.float32)\n            out = np.concatenate([out, pad]).astype(np.float32)\n        elif len(out) > self.dim:\n            out = out[:self.dim].astype(np.float32)\n        return out\n\n    def embed_many(self, seqs: List[str], v_calls: Optional[List[str]] = None, j_calls: Optional[List[str]] = None) -> np.ndarray:\n        v_calls = v_calls or [\"\"] * len(seqs)\n        j_calls = j_calls or [\"\"] * len(seqs)\n        mats = [self.embed_one(s, v, j) for s, v, j in zip(seqs, v_calls, j_calls)]\n        if len(mats) == 0:\n            return np.zeros((0, self.dim), dtype=np.float32)\n        return np.vstack(mats).astype(np.float32)\n\n\nclass LocalHFEmbedder:\n    def __init__(self, model_dir: str, batch_size: int = 16, max_length: int = 64):\n        self.model_dir = str(model_dir)\n        self.batch_size = int(batch_size)\n        self.max_length = int(max_length)\n        self.name = f\"local_hf::{Path(model_dir).name}\"\n\n        import torch\n        from transformers import AutoModel, AutoTokenizer\n\n        self.torch = torch\n        self.device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n        self.tokenizer = AutoTokenizer.from_pretrained(self.model_dir, local_files_only=True, trust_remote_code=True)\n        self.model = AutoModel.from_pretrained(self.model_dir, local_files_only=True, trust_remote_code=True)\n        self.model.to(self.device)\n        self.model.eval()\n\n        hidden = getattr(self.model.config, \"hidden_size\", None)\n        if hidden is None:\n            hidden = getattr(self.model.config, \"d_model\", None)\n        self.dim = int(hidden)\n\n    def _format_seq(self, seq: str) -> str:\n        # ProtBERT style often works better with spaced chars.\n        return \" \".join(list(seq))\n\n    def embed_many(self, seqs: List[str], v_calls: Optional[List[str]] = None, j_calls: Optional[List[str]] = None) -> np.ndarray:\n        if len(seqs) == 0:\n            return np.zeros((0, self.dim), dtype=np.float32)\n\n        out_chunks = []\n        with self.torch.no_grad():\n            for i in range(0, len(seqs), self.batch_size):\n                batch = seqs[i:i+self.batch_size]\n                batch_in = [self._format_seq(s) for s in batch]\n\n                toks = self.tokenizer(\n                    batch_in,\n                    padding=True,\n                    truncation=True,\n                    max_length=self.max_length,\n                    return_tensors=\"pt\",\n                )\n                toks = {k: v.to(self.device) for k, v in toks.items()}\n                res = self.model(**toks)\n\n                if hasattr(res, \"last_hidden_state\"):\n                    hs = res.last_hidden_state\n                else:\n                    hs = res[0]\n\n                attn = toks[\"attention_mask\"].unsqueeze(-1).float()\n                pooled = (hs * attn).sum(dim=1) / attn.sum(dim=1).clamp(min=1.0)\n                out_chunks.append(pooled.detach().cpu().numpy().astype(np.float32))\n\n        return np.vstack(out_chunks).astype(np.float32)\n\n\ndef create_embedder(route: str = \"auto\", local_model_dir: str = \"\", batch_size: int = 16, max_length: int = 64, fallback_dim: int = 128):\n    route = str(route).lower().strip()\n    local_model_dir = str(local_model_dir).strip()\n\n    if route in {\"local_hf\", \"auto\"} and local_model_dir and Path(local_model_dir).exists():\n        try:\n            embedder = LocalHFEmbedder(local_model_dir, batch_size=batch_size, max_length=max_length)\n            return embedder, {\"route_used\": \"local_hf\", \"embedder_name\": embedder.name, \"embedding_dim\": embedder.dim}\n        except Exception as e:\n            fallback = FallbackEmbedder(dim=fallback_dim)\n            return fallback, {\"route_used\": \"fallback_after_hf_error\", \"embedder_name\": fallback.name, \"embedding_dim\": fallback.dim, \"warning\": str(e)}\n\n    fallback = FallbackEmbedder(dim=fallback_dim)\n    return fallback, {\"route_used\": \"fallback\", \"embedder_name\": fallback.name, \"embedding_dim\": fallback.dim}\n\n\ndef build_repertoire_vector(\n    tsv_path: Path,\n    embedder,\n    max_sequences_per_file: int = 20000,\n    top_k_pool: int = 64,\n    random_state: int = 42,\n):\n    df = read_repertoire(tsv_path, max_seqs=max_sequences_per_file, random_state=random_state)\n    x = aggregate_unique_sequences(df)\n\n    if len(x) == 0:\n        dim = int(embedder.dim)\n        pooled = np.zeros(dim * 4 + 6, dtype=np.float32)\n        return pooled, {\n            \"n_unique_seq\": 0.0,\n            \"total_templates\": 0.0,\n            \"pool_topk_used\": 0.0,\n            \"embed_model\": embedder.name,\n            \"embed_dim\": float(dim),\n        }\n\n    seqs = x[\"junction_aa\"].astype(str).tolist()\n    v_calls = x[\"v_call\"].fillna(\"\").astype(str).tolist() if \"v_call\" in x.columns else [\"\"] * len(seqs)\n    j_calls = x[\"j_call\"].fillna(\"\").astype(str).tolist() if \"j_call\" in x.columns else [\"\"] * len(seqs)\n    weights = pd.to_numeric(x[\"templates\"], errors=\"coerce\").fillna(1.0).clip(lower=1.0).values.astype(np.float32)\n\n    E = embedder.embed_many(seqs, v_calls=v_calls, j_calls=j_calls)\n    if len(E) == 0:\n        dim = int(embedder.dim)\n        pooled = np.zeros(dim * 4 + 6, dtype=np.float32)\n        return pooled, {\n            \"n_unique_seq\": 0.0,\n            \"total_templates\": 0.0,\n            \"pool_topk_used\": 0.0,\n            \"embed_model\": embedder.name,\n            \"embed_dim\": float(dim),\n        }\n\n    w = weights / max(1e-9, weights.sum())\n    weighted_mean = np.average(E, axis=0, weights=w)\n\n    topk = min(int(top_k_pool), len(E))\n    idx_sorted = np.argsort(-weights)[:topk]\n    topk_mean = E[idx_sorted].mean(axis=0)\n\n    max_pool = E.max(axis=0)\n\n    centroid = weighted_mean\n    scores = (E @ centroid) / max(1e-6, np.linalg.norm(centroid)) + np.log1p(weights)\n    attn = softmax_np(scores)\n    attention_pool = np.average(E, axis=0, weights=attn)\n\n    stats = np.array([\n        float(len(seqs)),\n        float(weights.sum()),\n        float(topk),\n        float(np.mean([len(s) for s in seqs])),\n        float(np.std([len(s) for s in seqs])),\n        float(np.max(weights) / max(1e-9, weights.sum())),\n    ], dtype=np.float32)\n\n    pooled = np.concatenate([\n        weighted_mean.astype(np.float32),\n        topk_mean.astype(np.float32),\n        max_pool.astype(np.float32),\n        attention_pool.astype(np.float32),\n        stats.astype(np.float32),\n    ]).astype(np.float32)\n\n    meta = {\n        \"n_unique_seq\": float(len(seqs)),\n        \"total_templates\": float(weights.sum()),\n        \"pool_topk_used\": float(topk),\n        \"embed_model\": embedder.name,\n        \"embed_dim\": float(embedder.dim),\n    }\n    return pooled, meta\n\n\ndef build_embedding_table(\n    meta_df: pd.DataFrame,\n    ds_path: Path,\n    embedder,\n    max_sequences_per_file: int = 20000,\n    top_k_pool: int = 64,\n    random_state: int = 42,\n):\n    rows = []\n    for _, row in tqdm(meta_df.iterrows(), total=len(meta_df), leave=False):\n        vec, meta = build_repertoire_vector(\n            tsv_path=ds_path / row[\"filename\"],\n            embedder=embedder,\n            max_sequences_per_file=max_sequences_per_file,\n            top_k_pool=top_k_pool,\n            random_state=random_state,\n        )\n        record = {\n            \"ID\": row[\"repertoire_id\"],\n            \"dataset\": row[\"dataset_name\"],\n            \"label_positive\": int(row[\"label_positive\"]) if pd.notna(row[\"label_positive\"]) else pd.NA,\n            **meta,\n        }\n        for i, v in enumerate(vec):\n            record[f\"emb_{i:04d}\"] = float(v)\n        rows.append(record)\n    return pd.DataFrame(rows)\n\"\"\")\n\ntrain_branch_c_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport yaml\nfrom sklearn.decomposition import PCA\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.neural_network import MLPClassifier\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nPROJECT_ROOT = Path(__file__).resolve().parent\nif str(PROJECT_ROOT) not in sys.path:\n    sys.path.insert(0, str(PROJECT_ROOT))\n\nfrom src.branch_a_models import check_gpu, safe_auc\nfrom src.branch_c_embeddings import build_embedding_table, create_embedder\nfrom src.utils import ensure_dir, seed_everything, dataset_id_from_name\n\ntry:\n    import xgboost as xgb\n    HAS_XGB = True\nexcept Exception:\n    HAS_XGB = False\n\nSCALE_POS_WEIGHT = {\n    1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0, 6: 1.0, 7: 5.04, 8: 2.05\n}\n\n\ndef train_lr(X_tr, y_tr, X_va):\n    pipe = Pipeline([\n        (\"scaler\", StandardScaler()),\n        (\"lr\", LogisticRegression(\n            C=0.8,\n            max_iter=3000,\n            solver=\"liblinear\",\n            class_weight=\"balanced\",\n            random_state=42,\n        )),\n    ])\n    pipe.fit(X_tr, y_tr)\n    pred = pipe.predict_proba(X_va)[:, 1]\n    return pipe, pred\n\n\ndef train_mlp(X_tr, y_tr, X_va, seed=42):\n    pipe = Pipeline([\n        (\"scaler\", StandardScaler()),\n        (\"mlp\", MLPClassifier(\n            hidden_layer_sizes=(128, 64),\n            activation=\"relu\",\n            alpha=1e-4,\n            learning_rate_init=1e-3,\n            batch_size=32,\n            max_iter=300,\n            early_stopping=True,\n            random_state=seed,\n        )),\n    ])\n    pipe.fit(X_tr, y_tr)\n    pred = pipe.predict_proba(X_va)[:, 1]\n    return pipe, pred\n\n\ndef train_xgb(X_tr, y_tr, X_va, y_va, ds_scale, seed=42, gpu_ok=False):\n    if not HAS_XGB:\n        return None, None\n\n    dtr = xgb.DMatrix(X_tr, label=y_tr)\n    dva = xgb.DMatrix(X_va, label=y_va)\n\n    params = {\n        \"objective\": \"binary:logistic\",\n        \"eval_metric\": \"auc\",\n        \"max_depth\": 5,\n        \"learning_rate\": 0.05,\n        \"subsample\": 0.9,\n        \"colsample_bytree\": 0.85,\n        \"min_child_weight\": 5,\n        \"reg_alpha\": 0.10,\n        \"reg_lambda\": 1.00,\n        \"scale_pos_weight\": ds_scale,\n        \"tree_method\": \"hist\",\n        \"device\": \"cuda\" if gpu_ok else \"cpu\",\n        \"seed\": seed,\n        \"verbosity\": 0,\n    }\n\n    try:\n        bst = xgb.train(\n            params,\n            dtr,\n            num_boost_round=1000,\n            evals=[(dva, \"valid\")],\n            early_stopping_rounds=60,\n            verbose_eval=False,\n        )\n    except Exception:\n        params[\"device\"] = \"cpu\"\n        bst = xgb.train(\n            params,\n            dtr,\n            num_boost_round=1000,\n            evals=[(dva, \"valid\")],\n            early_stopping_rounds=60,\n            verbose_eval=False,\n        )\n\n    pred = bst.predict(dva)\n    return bst, pred\n\n\ndef save_pca_plot(emb_df: pd.DataFrame, out_path: Path):\n    feature_cols = [c for c in emb_df.columns if c.startswith(\"emb_\")]\n    if len(feature_cols) < 2 or len(emb_df) == 0:\n        return\n\n    X = emb_df[feature_cols].fillna(0.0).values\n    y = pd.to_numeric(emb_df[\"label_positive\"], errors=\"coerce\").fillna(-1).astype(int).values\n\n    pca = PCA(n_components=2, random_state=42)\n    Z = pca.fit_transform(X)\n\n    plt.figure(figsize=(8, 6))\n    mask_pos = y == 1\n    mask_neg = y == 0\n    mask_other = ~(mask_pos | mask_neg)\n\n    if mask_neg.any():\n        plt.scatter(Z[mask_neg, 0], Z[mask_neg, 1], s=15, alpha=0.6, label=\"negative\")\n    if mask_pos.any():\n        plt.scatter(Z[mask_pos, 0], Z[mask_pos, 1], s=15, alpha=0.6, label=\"positive\")\n    if mask_other.any():\n        plt.scatter(Z[mask_other, 0], Z[mask_other, 1], s=15, alpha=0.6, label=\"unknown\")\n\n    plt.xlabel(f\"PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)\")\n    plt.ylabel(f\"PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)\")\n    plt.title(\"Branch C Embedding PCA\")\n    plt.legend()\n    plt.tight_layout()\n    plt.savefig(out_path, dpi=180, bbox_inches=\"tight\")\n    plt.close()\n\n\ndef main():\n    cfg_base = yaml.safe_load((PROJECT_ROOT / \"configs\" / \"base.yaml\").read_text())\n    cfg_branch = yaml.safe_load((PROJECT_ROOT / \"configs\" / \"branch_c.yaml\").read_text())\n\n    runtime = cfg_base[\"runtime\"]\n    phase1_paths = cfg_base[\"paths\"]\n    out_dir = ensure_dir(cfg_branch[\"paths\"][\"output_root\"])\n\n    seed_everything(runtime[\"random_state\"])\n    gpu_ok = check_gpu()\n\n    canonical = pd.read_csv(Path(phase1_paths[\"output_root\"]) / \"canonical_metadata.csv\")\n    all_folds = pd.read_csv(Path(phase1_paths[\"output_root\"]) / \"all_folds.csv\")\n    train_root = Path(phase1_paths[\"train_root\"])\n\n    embedder, embedder_info = create_embedder(\n        route=cfg_branch[\"model\"][\"route\"],\n        local_model_dir=cfg_branch[\"model\"][\"local_model_dir\"],\n        batch_size=cfg_branch[\"model\"][\"batch_size\"],\n        max_length=cfg_branch[\"model\"][\"max_length\"],\n        fallback_dim=cfg_branch[\"model\"][\"fallback_dim\"],\n    )\n\n    print(\"=\" * 80)\n    print(\"PHASE 4: Branch C - pretrained sequence embedding model\")\n    print(\"=\" * 80)\n    print(\"Embedder info:\", embedder_info)\n    print(\"GPU available:\", gpu_ok)\n\n    train_meta_all = canonical[canonical[\"source\"] == \"train\"].copy()\n    train_meta_all = train_meta_all[pd.notna(train_meta_all[\"label_positive\"])].copy()\n    dataset_names = sorted(train_meta_all[\"dataset_name\"].unique().tolist())\n\n    seeds = list(cfg_branch[\"training\"][\"seeds\"])\n\n    all_emb = []\n    all_oof = []\n    all_summary = []\n\n    for ds_name in dataset_names:\n        ds_id = dataset_id_from_name(ds_name)\n        ds_scale = SCALE_POS_WEIGHT.get(ds_id, 1.0)\n        ds_path = train_root / ds_name\n\n        print(f\"\\\\n{'-' * 80}\")\n        print(f\"Branch C training: {ds_name} (id={ds_id})\")\n        print(f\"{'-' * 80}\")\n\n        meta_ds = train_meta_all[train_meta_all[\"dataset_name\"] == ds_name].copy()\n        fold_ds = all_folds[all_folds[\"dataset_name\"] == ds_name].copy()\n\n        meta_ds = meta_ds.merge(\n            fold_ds[[\"dataset_name\", \"repertoire_id\", \"fold\", \"splitter\", \"n_splits_used\"]],\n            on=[\"dataset_name\", \"repertoire_id\"],\n            how=\"inner\",\n        ).reset_index(drop=True)\n\n        emb_df = build_embedding_table(\n            meta_df=meta_ds,\n            ds_path=ds_path,\n            embedder=embedder,\n            max_sequences_per_file=cfg_branch[\"embedding\"][\"max_sequences_per_file\"],\n            top_k_pool=cfg_branch[\"embedding\"][\"top_k_pool\"],\n            random_state=runtime[\"random_state\"],\n        )\n        emb_df[\"dataset_id\"] = ds_id\n        emb_df[\"embed_route_used\"] = embedder_info.get(\"route_used\", \"unknown\")\n        emb_df[\"fold\"] = meta_ds[\"fold\"].values\n\n        emb_df.to_csv(out_dir / f\"embedding_vectors_{ds_name}.csv\", index=False)\n        all_emb.append(emb_df)\n\n        feature_cols = [c for c in emb_df.columns if c.startswith(\"emb_\")]\n        X_all = emb_df[feature_cols].fillna(0.0).astype(np.float32).values\n        y = emb_df[\"label_positive\"].astype(int).values\n\n        seed_frames = []\n        summary_rows = []\n        model_cols = [\"LR\", \"XGB\", \"MLP\"]\n\n        unique_folds = sorted(emb_df[\"fold\"].dropna().unique().tolist())\n\n        for seed in seeds:\n            pred_store = {\n                \"ID\": emb_df[\"ID\"].astype(str).tolist(),\n                \"dataset\": emb_df[\"dataset\"].astype(str).tolist(),\n                \"label_positive\": y.tolist(),\n                \"fold\": emb_df[\"fold\"].tolist(),\n                \"seed\": [seed] * len(emb_df),\n            }\n            for m in model_cols:\n                pred_store[m] = [np.nan] * len(emb_df)\n\n            for fold in unique_folds:\n                tr_idx = emb_df.index[emb_df[\"fold\"] != fold].to_numpy()\n                va_idx = emb_df.index[emb_df[\"fold\"] == fold].to_numpy()\n\n                X_tr = X_all[tr_idx]\n                X_va = X_all[va_idx]\n                y_tr = y[tr_idx]\n                y_va = y[va_idx]\n\n                _, pred_lr = train_lr(X_tr, y_tr, X_va)\n                for idx, p in zip(va_idx, pred_lr):\n                    pred_store[\"LR\"][idx] = float(p)\n                summary_rows.append({\"dataset\": ds_name, \"seed\": seed, \"fold\": fold, \"model\": \"LR\", \"auc\": safe_auc(y_va, pred_lr)})\n\n                model, pred_xgb = train_xgb(X_tr, y_tr, X_va, y_va, ds_scale, seed=seed, gpu_ok=gpu_ok)\n                if pred_xgb is not None:\n                    for idx, p in zip(va_idx, pred_xgb):\n                        pred_store[\"XGB\"][idx] = float(p)\n                    summary_rows.append({\"dataset\": ds_name, \"seed\": seed, \"fold\": fold, \"model\": \"XGB\", \"auc\": safe_auc(y_va, pred_xgb)})\n\n                _, pred_mlp = train_mlp(X_tr, y_tr, X_va, seed=seed)\n                for idx, p in zip(va_idx, pred_mlp):\n                    pred_store[\"MLP\"][idx] = float(p)\n                summary_rows.append({\"dataset\": ds_name, \"seed\": seed, \"fold\": fold, \"model\": \"MLP\", \"auc\": safe_auc(y_va, pred_mlp)})\n\n            pred_df = pd.DataFrame(pred_store)\n            valid_model_cols = [c for c in model_cols if pred_df[c].notna().any()]\n            pred_df[\"AVG\"] = pred_df[valid_model_cols].mean(axis=1)\n            seed_frames.append(pred_df)\n\n            for m in valid_model_cols + [\"AVG\"]:\n                auc_full = safe_auc(pred_df[\"label_positive\"].values, pred_df[m].values)\n                summary_rows.append({\"dataset\": ds_name, \"seed\": seed, \"fold\": -1, \"model\": m, \"auc\": auc_full})\n\n        ds_oof = pd.concat(seed_frames, ignore_index=True)\n        ds_oof.to_csv(out_dir / f\"branch_c_oof_{ds_name}.csv\", index=False)\n        all_oof.append(ds_oof)\n\n        summary_df = pd.DataFrame(summary_rows)\n        if len(summary_df):\n            summary_df.to_csv(out_dir / f\"branch_c_summary_{ds_name}.csv\", index=False)\n            overall = (\n                summary_df[summary_df[\"fold\"] >= 0]\n                .groupby([\"dataset\", \"seed\", \"model\"], as_index=False)[\"auc\"]\n                .agg(mean_auc=\"mean\", std_auc=\"std\", max_auc=\"max\")\n                .sort_values([\"dataset\", \"seed\", \"mean_auc\"], ascending=[True, True, False])\n            )\n            print(\"\\\\nSummary:\")\n            print(overall.to_string(index=False))\n        all_summary.append(summary_df)\n\n    final_emb = pd.concat(all_emb, ignore_index=True) if len(all_emb) else pd.DataFrame()\n    final_oof = pd.concat(all_oof, ignore_index=True) if len(all_oof) else pd.DataFrame()\n    final_summary = pd.concat(all_summary, ignore_index=True) if len(all_summary) else pd.DataFrame()\n\n    # save parquet with safe fallback\n    parquet_path = out_dir / \"embedding_repertoire_vectors.parquet\"\n    if len(final_emb):\n        try:\n            final_emb.to_parquet(parquet_path, index=False)\n        except Exception:\n            final_emb.to_csv(out_dir / \"embedding_repertoire_vectors_fallback.csv\", index=False)\n\n    final_oof.to_csv(out_dir / \"branch_c_oof.csv\", index=False)\n\n    if len(final_summary):\n        agg = (\n            final_summary[final_summary[\"fold\"] >= 0]\n            .groupby([\"dataset\", \"seed\", \"model\"], as_index=False)[\"auc\"]\n            .agg(mean_auc=\"mean\", std_auc=\"std\", max_auc=\"max\")\n            .sort_values([\"dataset\", \"seed\", \"mean_auc\"], ascending=[True, True, False])\n        )\n        agg.to_csv(out_dir / \"branch_c_summary.csv\", index=False)\n    else:\n        pd.DataFrame().to_csv(out_dir / \"branch_c_summary.csv\", index=False)\n\n    if len(final_emb):\n        save_pca_plot(final_emb, out_dir / \"embedding_pca_plot.png\")\n\n        # optional UMAP if installed\n        try:\n            import umap\n            feature_cols = [c for c in final_emb.columns if c.startswith(\"emb_\")]\n            X = final_emb[feature_cols].fillna(0.0).values\n            y = pd.to_numeric(final_emb[\"label_positive\"], errors=\"coerce\").fillna(-1).astype(int).values\n            reducer = umap.UMAP(n_components=2, random_state=42)\n            Z = reducer.fit_transform(X)\n\n            plt.figure(figsize=(8, 6))\n            mask_pos = y == 1\n            mask_neg = y == 0\n            if mask_neg.any():\n                plt.scatter(Z[mask_neg, 0], Z[mask_neg, 1], s=15, alpha=0.6, label=\"negative\")\n            if mask_pos.any():\n                plt.scatter(Z[mask_pos, 0], Z[mask_pos, 1], s=15, alpha=0.6, label=\"positive\")\n            plt.title(\"Branch C Embedding UMAP\")\n            plt.legend()\n            plt.tight_layout()\n            plt.savefig(out_dir / \"embedding_umap_plot.png\", dpi=180, bbox_inches=\"tight\")\n            plt.close()\n        except Exception:\n            pass\n\n    print(\"\\\\n\" + \"=\" * 80)\n    print(\"PHASE 4 DONE\")\n    print(\"Saved:\")\n    print(out_dir / \"branch_c_oof.csv\")\n    print(out_dir / \"embedding_repertoire_vectors.parquet\")\n    print(out_dir / \"embedding_pca_plot.png\")\n    print(\"=\" * 80)\n\n\nif __name__ == \"__main__\":\n    main()\n\"\"\")\n\n(PROJECT_ROOT / \"src\" / \"branch_c_embeddings.py\").write_text(branch_c_embeddings_py, encoding=\"utf-8\")\n(PROJECT_ROOT / \"train_branch_c.py\").write_text(train_branch_c_py, encoding=\"utf-8\")\n\nprint(\"Written:\")\nprint(\"-\", PROJECT_ROOT / \"src\" / \"branch_c_embeddings.py\")\nprint(\"-\", PROJECT_ROOT / \"train_branch_c.py\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:05:58.523897Z","iopub.execute_input":"2026-07-24T11:05:58.524984Z","iopub.status.idle":"2026-07-24T11:05:58.545670Z","shell.execute_reply.started":"2026-07-24T11:05:58.524936Z","shell.execute_reply":"2026-07-24T11:05:58.544985Z"}},"outputs":[],"execution_count":null},{"id":"a79ebdbe","cell_type":"code","source":"#Cell 3 — imports verify করো\nfrom pathlib import Path\np = Path(\"/kaggle/working/project/src\")\nprint(sorted(f.name for f in p.glob(\"*.py\")))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:11:30.474132Z","iopub.execute_input":"2026-07-24T11:11:30.474559Z","iopub.status.idle":"2026-07-24T11:11:30.480181Z","shell.execute_reply.started":"2026-07-24T11:11:30.474527Z","shell.execute_reply":"2026-07-24T11:11:30.479401Z"}},"outputs":[],"execution_count":null},{"id":"e89d6a07","cell_type":"code","source":"#Cell 4 — Phase 4 run করো\n!python /kaggle/working/project/train_branch_c.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:11:31.098303Z","iopub.execute_input":"2026-07-24T11:11:31.098709Z","iopub.status.idle":"2026-07-24T11:11:33.631967Z","shell.execute_reply.started":"2026-07-24T11:11:31.098680Z","shell.execute_reply":"2026-07-24T11:11:33.630987Z"}},"outputs":[],"execution_count":null},{"id":"226f71ee","cell_type":"code","source":"#Cell 5 — outputs inspect করো\nfrom pathlib import Path\nimport pandas as pd\n\nOUT_DIR = Path(\"/kaggle/working/project/artifacts/phase3_branch_b\")\n\nprint(\"OUT_DIR exists:\", OUT_DIR.exists())\nprint(\"Files:\")\nfound = sorted(OUT_DIR.glob(\"*\")) if OUT_DIR.exists() else []\nfor p in found:\n    print(\"-\", p.name)\nif not found:\n    print(\"(none — Branch B training/inference did not write any outputs here.)\")\n\ndef show(name, path):\n    print(f\"\\n{name}\")\n    if path.exists():\n        display(pd.read_csv(path).head())\n    else:\n        print(f\"  -> missing: {path}\")\n\nshow(\"branch_b_oof.csv\", OUT_DIR / \"branch_b_oof.csv\")\n\nsummary_path = OUT_DIR / \"branch_b_summary.csv\"\nif summary_path.exists():\n    print(\"\\nbranch_b_summary.csv\")\n    display(pd.read_csv(summary_path))\nelse:\n    print(\"\\nbranch_b_summary.csv -> missing\")\n\nexact_files = sorted(OUT_DIR.glob(\"disease_enriched_sequences_*.csv\"))\nif exact_files:\n    print(f\"\\n{exact_files[0].name}\")\n    display(pd.read_csv(exact_files[0]).head())\nelse:\n    print(\"\\ndisease_enriched_sequences_*.csv -> none found\")\n\ncluster_files = sorted(OUT_DIR.glob(\"cluster_catalog_*.csv\"))\nif cluster_files:\n    print(f\"\\n{cluster_files[0].name}\")\n    display(pd.read_csv(cluster_files[0]).head())\nelse:\n    print(\"\\ncluster_catalog_*.csv -> none found\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:11:42.240794Z","iopub.execute_input":"2026-07-24T11:11:42.241372Z","iopub.status.idle":"2026-07-24T11:11:42.250736Z","shell.execute_reply.started":"2026-07-24T11:11:42.241342Z","shell.execute_reply":"2026-07-24T11:11:42.249985Z"}},"outputs":[],"execution_count":null},{"id":"d639db08-4f6c-4f4b-acf4-b352304f27ff","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"a8c7b8d5-4fa6-461b-a1ac-a31a7ca8bb06","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}