{"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":"096c565a","cell_type":"markdown","source":"# Phase 6 — Explicit Sequence Attribution Head (fused multi-signal ranking)\n\nFuses multiple attribution signals into a final ranked sequence score: attention weight, leave-one-clone-out delta prediction, gradient / integrated-gradient score, public-clone enrichment, cluster membership. Avoids the 'attention = explanation' trap. Outputs phase6_attribution_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- Phase 5 notebook output (Branch D trained MIL state)\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":"37f9c012","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:21:27.047071Z","iopub.execute_input":"2026-07-24T11:21:27.047434Z","iopub.status.idle":"2026-07-24T11:21:27.059610Z","shell.execute_reply.started":"2026-07-24T11:21:27.047405Z","shell.execute_reply":"2026-07-24T11:21:27.058772Z"}},"outputs":[],"execution_count":null},{"id":"80c49e90","cell_type":"code","source":"#Cell 1 — attribution.yaml write করো\nfrom pathlib import Path\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nattribution_yaml = \"\"\"\nenabled: true\n\npaths:\n  output_root: /kaggle/working/project/artifacts/phase6_attribution\n\ndictionary:\n  max_sequences_per_file: 30000\n  max_repertoires_per_class: 150\n  min_freq: 0.10\n  enrichment: 3.0\n  top_exact: 3000\n  top_clusters: 5000\n\nbag:\n  top_k_abundant: 256\n  random_tail: 128\n  force_public_top: 32\n  max_seq_len: 32\n\nmodel:\n  aa_emb_dim: 32\n  gene_emb_dim: 8\n  conv_channels: 64\n  hidden_dim: 128\n  dropout: 0.20\n\ntraining:\n  seeds: [42, 52, 62]\n  batch_size: 16\n  epochs: 8\n  lr: 0.001\n  weight_decay: 0.0001\n  lambda_attr: 0.25\n  lambda_sparse: 0.01\n\nattribution:\n  ig_steps: 8\n  max_loo_sequences: 128\n  rank_weights:\n    attention: 0.30\n    loo_delta: 0.20\n    grad: 0.20\n    public: 0.15\n    cluster: 0.15\n\"\"\"\n\n(PROJECT_ROOT / \"configs\" / \"attribution.yaml\").write_text(attribution_yaml.strip() + \"\\n\", encoding=\"utf-8\")\nprint(\"Written:\", PROJECT_ROOT / \"configs\" / \"attribution.yaml\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:21:27.060988Z","iopub.execute_input":"2026-07-24T11:21:27.061748Z","iopub.status.idle":"2026-07-24T11:21:27.087441Z","shell.execute_reply.started":"2026-07-24T11:21:27.061724Z","shell.execute_reply":"2026-07-24T11:21:27.086720Z"}},"outputs":[],"execution_count":null},{"id":"dc09098a","cell_type":"code","source":"#Cell 2 — src/attribution.py আর train_attribution.py write করো\nfrom pathlib import Path\nfrom textwrap import dedent\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nattribution_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Dict, List\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader\n\nfrom .branch_b_cluster import approx_cluster_keys\nfrom .branch_b_public import build_dataset_cache, build_fold_signal_catalog\nfrom .branch_d_mil import (\n    DeepMILModel,\n    RepertoireBagDataset,\n    build_bag_records_from_meta,\n    build_repertoire_table_cache,\n    compute_losses,\n    seed_torch,\n)\n\n\ndef collate_fn(batch):\n    return {\n        \"ID\": [x[\"ID\"] for x in batch],\n        \"dataset\": [x[\"dataset\"] for x in batch],\n        \"label\": torch.stack([x[\"label\"] for x in batch]),\n        \"tokens\": torch.stack([x[\"tokens\"] for x in batch]),\n        \"v_ids\": torch.stack([x[\"v_ids\"] for x in batch]),\n        \"j_ids\": torch.stack([x[\"j_ids\"] for x in batch]),\n        \"abundance\": torch.stack([x[\"abundance\"] for x in batch]),\n        \"lengths\": torch.stack([x[\"lengths\"] for x in batch]),\n        \"weak_targets\": torch.stack([x[\"weak_targets\"] for x in batch]),\n        \"mask\": torch.stack([x[\"mask\"] for x in batch]),\n        \"seqs\": [x[\"seqs\"] for x in batch],\n    }\n\n\ndef move_to_device(batch, device):\n    out = {}\n    for k, v in batch.items():\n        if torch.is_tensor(v):\n            out[k] = v.to(device)\n        else:\n            out[k] = v\n    return out\n\n\ndef train_mil_one_fold(train_records, val_records, cfg_model, cfg_train, device, seed):\n    seed_torch(seed)\n\n    train_ds = RepertoireBagDataset(train_records)\n    val_ds = RepertoireBagDataset(val_records)\n\n    train_loader = DataLoader(train_ds, batch_size=cfg_train[\"batch_size\"], shuffle=True, collate_fn=collate_fn)\n    val_loader = DataLoader(val_ds, batch_size=cfg_train[\"batch_size\"], shuffle=False, collate_fn=collate_fn)\n\n    model = DeepMILModel(\n        aa_emb_dim=cfg_model[\"aa_emb_dim\"],\n        gene_emb_dim=cfg_model[\"gene_emb_dim\"],\n        conv_channels=cfg_model[\"conv_channels\"],\n        hidden_dim=cfg_model[\"hidden_dim\"],\n        dropout=cfg_model[\"dropout\"],\n    ).to(device)\n\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=cfg_train[\"lr\"],\n        weight_decay=cfg_train[\"weight_decay\"],\n    )\n\n    best_state = None\n    best_auc = -np.inf\n    best_epoch = -1\n\n    for epoch in range(cfg_train[\"epochs\"]):\n        model.train()\n        for batch in train_loader:\n            batch_dev = move_to_device(batch, device)\n            outputs = model(\n                tokens=batch_dev[\"tokens\"],\n                v_ids=batch_dev[\"v_ids\"],\n                j_ids=batch_dev[\"j_ids\"],\n                abundance=batch_dev[\"abundance\"],\n                lengths=batch_dev[\"lengths\"],\n                mask=batch_dev[\"mask\"],\n            )\n            losses = compute_losses(\n                outputs=outputs,\n                labels=batch_dev[\"label\"],\n                weak_targets=batch_dev[\"weak_targets\"],\n                mask=batch_dev[\"mask\"],\n                lambda_attr=cfg_train[\"lambda_attr\"],\n                lambda_sparse=cfg_train[\"lambda_sparse\"],\n            )\n\n            optimizer.zero_grad()\n            losses[\"total\"].backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)\n            optimizer.step()\n\n        val_auc, _ = evaluate_bag_auc(model, val_loader, device)\n        if np.isnan(val_auc):\n            val_auc = -1.0\n\n        if val_auc > best_auc:\n            best_auc = val_auc\n            best_epoch = epoch\n            best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}\n\n    if best_state is not None:\n        model.load_state_dict(best_state)\n\n    final_auc, final_probs = evaluate_bag_auc(model, val_loader, device)\n    return model, final_auc, final_probs, best_epoch\n\n\ndef evaluate_bag_auc(model, loader, device):\n    model.eval()\n    bag_probs = []\n    bag_labels = []\n\n    with torch.no_grad():\n        for batch in loader:\n            batch_dev = move_to_device(batch, device)\n            outputs = model(\n                tokens=batch_dev[\"tokens\"],\n                v_ids=batch_dev[\"v_ids\"],\n                j_ids=batch_dev[\"j_ids\"],\n                abundance=batch_dev[\"abundance\"],\n                lengths=batch_dev[\"lengths\"],\n                mask=batch_dev[\"mask\"],\n            )\n            probs = torch.sigmoid(outputs[\"bag_logit\"]).detach().cpu().numpy()\n            bag_probs.extend(probs.tolist())\n            bag_labels.extend(batch[\"label\"].numpy().tolist())\n\n    bag_probs = np.asarray(bag_probs, dtype=float)\n    bag_labels = np.asarray(bag_labels, dtype=float)\n\n    if len(np.unique(bag_labels)) < 2:\n        return np.nan, bag_probs\n\n    from sklearn.metrics import roc_auc_score\n    auc = float(roc_auc_score(bag_labels, bag_probs))\n    return auc, bag_probs\n\n\ndef forward_from_seq_repr(model, seq_repr, mask):\n    attn_logits = model.attn(seq_repr).squeeze(-1)\n    attn_logits = attn_logits.masked_fill(mask <= 0, -1e9)\n    attn_weights = torch.softmax(attn_logits, dim=1)\n    bag_repr = torch.sum(seq_repr * attn_weights.unsqueeze(-1), dim=1)\n    bag_logit = model.cls_head(bag_repr).squeeze(-1)\n    return bag_logit, attn_weights\n\n\ndef integrated_gradient_scores(model, tokens, v_ids, j_ids, abundance, lengths, mask, steps=8):\n    model.eval()\n\n    seq_repr = model.encoder(tokens, v_ids, j_ids, abundance, lengths)  # [1, N, H]\n    baseline = torch.zeros_like(seq_repr)\n    total_grad = torch.zeros_like(seq_repr)\n\n    alphas = torch.linspace(1.0 / steps, 1.0, steps, device=seq_repr.device)\n\n    for alpha in alphas:\n        interp = (baseline + alpha * (seq_repr - baseline)).detach().requires_grad_(True)\n        bag_logit, _ = forward_from_seq_repr(model, interp, mask)\n        grads = torch.autograd.grad(bag_logit.sum(), interp, retain_graph=False, create_graph=False)[0]\n        total_grad += grads\n\n    ig = (seq_repr - baseline) * total_grad / float(steps)\n    score = ig.abs().sum(dim=-1).squeeze(0) * mask.squeeze(0)\n    return score.detach().cpu().numpy().astype(np.float32)\n\n\ndef public_cluster_scores_for_sequences(seqs: List[str], bundle: dict):\n    exact_catalog = bundle.get(\"exact_catalog\", {})\n    cluster_catalog = bundle.get(\"cluster_catalog\", {})\n\n    public_scores = []\n    cluster_scores = []\n\n    for seq in seqs:\n        ps = 0.0\n        cs = 0.0\n\n        if seq in exact_catalog:\n            ps = float(exact_catalog[seq][\"score\"])\n\n        for key in approx_cluster_keys(seq):\n            if key in cluster_catalog:\n                cs = max(cs, float(cluster_catalog[key][\"score\"]))\n\n        public_scores.append(ps)\n        cluster_scores.append(cs)\n\n    return np.asarray(public_scores, dtype=np.float32), np.asarray(cluster_scores, dtype=np.float32)\n\n\ndef leave_one_out_scores(model, tokens, v_ids, j_ids, abundance, lengths, mask, attention_scores, weak_targets, max_loo_sequences=128):\n    model.eval()\n\n    with torch.no_grad():\n        base_out = model(\n            tokens=tokens,\n            v_ids=v_ids,\n            j_ids=j_ids,\n            abundance=abundance,\n            lengths=lengths,\n            mask=mask,\n        )\n        base_prob = torch.sigmoid(base_out[\"bag_logit\"]).detach().cpu().item()\n\n    valid_idx = np.where(mask.squeeze(0).detach().cpu().numpy() > 0)[0].tolist()\n    if len(valid_idx) == 0:\n        return np.zeros(mask.shape[1], dtype=np.float32)\n\n    att = np.asarray(attention_scores, dtype=float)\n    weak = np.asarray(weak_targets, dtype=float)\n    priority = att + weak\n    cand = sorted(valid_idx, key=lambda i: -priority[i])[:max_loo_sequences]\n\n    loo = np.zeros(mask.shape[1], dtype=np.float32)\n\n    with torch.no_grad():\n        for j in cand:\n            mod_mask = mask.clone()\n            mod_mask[:, j] = 0.0\n            out = model(\n                tokens=tokens,\n                v_ids=v_ids,\n                j_ids=j_ids,\n                abundance=abundance,\n                lengths=lengths,\n                mask=mod_mask,\n            )\n            p = torch.sigmoid(out[\"bag_logit\"]).detach().cpu().item()\n            loo[j] = max(0.0, float(base_prob - p))\n\n    return loo.astype(np.float32)\n\n\ndef rank_normalize(scores: np.ndarray, mask: np.ndarray) -> np.ndarray:\n    scores = np.asarray(scores, dtype=float)\n    mask = np.asarray(mask, dtype=float)\n    out = np.zeros_like(scores, dtype=np.float32)\n\n    valid = np.where(mask > 0)[0]\n    if len(valid) == 0:\n        return out\n\n    vals = scores[valid]\n    order = np.argsort(-vals, kind=\"mergesort\")\n    if len(valid) == 1:\n        out[valid[order[0]]] = 1.0\n        return out\n\n    for rank_pos, idx_pos in enumerate(order):\n        score_norm = 1.0 - (rank_pos / (len(valid) - 1))\n        out[valid[idx_pos]] = float(score_norm)\n    return out\n\n\ndef fuse_rank_scores(attention_score, loo_delta, grad_score, public_score, cluster_score, mask, rank_weights):\n    att_rank = rank_normalize(attention_score, mask)\n    loo_rank = rank_normalize(loo_delta, mask)\n    grad_rank = rank_normalize(grad_score, mask)\n    pub_rank = rank_normalize(public_score, mask)\n    clu_rank = rank_normalize(cluster_score, mask)\n\n    final = (\n        rank_weights[\"attention\"] * att_rank +\n        rank_weights[\"loo_delta\"] * loo_rank +\n        rank_weights[\"grad\"] * grad_rank +\n        rank_weights[\"public\"] * pub_rank +\n        rank_weights[\"cluster\"] * clu_rank\n    ) * mask\n\n    return {\n        \"attention_rank\": att_rank.astype(np.float32),\n        \"loo_rank\": loo_rank.astype(np.float32),\n        \"grad_rank\": grad_rank.astype(np.float32),\n        \"public_rank\": pub_rank.astype(np.float32),\n        \"cluster_rank\": clu_rank.astype(np.float32),\n        \"final_rank_score\": final.astype(np.float32),\n    }\n\n\ndef attribute_one_record(model, record: dict, bundle: dict, device, ig_steps=8, max_loo_sequences=128, rank_weights=None):\n    rank_weights = rank_weights or {\n        \"attention\": 0.30,\n        \"loo_delta\": 0.20,\n        \"grad\": 0.20,\n        \"public\": 0.15,\n        \"cluster\": 0.15,\n    }\n\n    tokens = torch.tensor(record[\"tokens\"], dtype=torch.long, device=device).unsqueeze(0)\n    v_ids = torch.tensor(record[\"v_ids\"], dtype=torch.long, device=device).unsqueeze(0)\n    j_ids = torch.tensor(record[\"j_ids\"], dtype=torch.long, device=device).unsqueeze(0)\n    abundance = torch.tensor(record[\"abundance\"], dtype=torch.float32, device=device).unsqueeze(0)\n    lengths = torch.tensor(record[\"lengths\"], dtype=torch.float32, device=device).unsqueeze(0)\n    weak_targets = np.asarray(record[\"weak_targets\"], dtype=np.float32)\n    mask = torch.tensor(record[\"mask\"], dtype=torch.float32, device=device).unsqueeze(0)\n\n    model.eval()\n    with torch.no_grad():\n        out = model(\n            tokens=tokens,\n            v_ids=v_ids,\n            j_ids=j_ids,\n            abundance=abundance,\n            lengths=lengths,\n            mask=mask,\n        )\n        bag_prob = torch.sigmoid(out[\"bag_logit\"]).detach().cpu().item()\n        attention = out[\"attn_weights\"].detach().cpu().numpy().squeeze(0).astype(np.float32)\n\n    grad_score = integrated_gradient_scores(\n        model=model,\n        tokens=tokens,\n        v_ids=v_ids,\n        j_ids=j_ids,\n        abundance=abundance,\n        lengths=lengths,\n        mask=mask,\n        steps=ig_steps,\n    )\n\n    loo_delta = leave_one_out_scores(\n        model=model,\n        tokens=tokens,\n        v_ids=v_ids,\n        j_ids=j_ids,\n        abundance=abundance,\n        lengths=lengths,\n        mask=mask,\n        attention_scores=attention,\n        weak_targets=weak_targets,\n        max_loo_sequences=max_loo_sequences,\n    )\n\n    public_score, cluster_score = public_cluster_scores_for_sequences(record[\"seqs\"], bundle)\n    mask_np = np.asarray(record[\"mask\"], dtype=np.float32)\n\n    fused = fuse_rank_scores(\n        attention_score=attention,\n        loo_delta=loo_delta,\n        grad_score=grad_score,\n        public_score=public_score,\n        cluster_score=cluster_score,\n        mask=mask_np,\n        rank_weights=rank_weights,\n    )\n\n    valid_idx = np.where(mask_np > 0)[0].tolist()\n    rows = []\n    for j in valid_idx:\n        rows.append({\n            \"ID\": record[\"ID\"],\n            \"dataset\": record[\"dataset\"],\n            \"label_positive\": int(record[\"label_positive\"]),\n            \"sequence\": record[\"seqs\"][j],\n            \"attention_score\": float(attention[j]),\n            \"loo_delta\": float(loo_delta[j]),\n            \"grad_score\": float(grad_score[j]),\n            \"public_score\": float(public_score[j]),\n            \"cluster_score\": float(cluster_score[j]),\n            \"attention_rank\": float(fused[\"attention_rank\"][j]),\n            \"loo_rank\": float(fused[\"loo_rank\"][j]),\n            \"grad_rank\": float(fused[\"grad_rank\"][j]),\n            \"public_rank\": float(fused[\"public_rank\"][j]),\n            \"cluster_rank\": float(fused[\"cluster_rank\"][j]),\n            \"rank_score\": float(fused[\"final_rank_score\"][j]),\n            \"bag_probability\": float(bag_prob),\n        })\n\n    rows = sorted(rows, key=lambda x: (-x[\"rank_score\"], -x[\"attention_score\"], -x[\"public_score\"]))\n    for k, row in enumerate(rows, start=1):\n        row[\"rank\"] = int(k)\n\n    return rows, {\n        \"ID\": record[\"ID\"],\n        \"attention\": attention.astype(np.float32),\n        \"loo_delta\": loo_delta.astype(np.float32),\n        \"grad_score\": grad_score.astype(np.float32),\n        \"public_score\": public_score.astype(np.float32),\n        \"cluster_score\": cluster_score.astype(np.float32),\n        \"mask\": mask_np.astype(np.float32),\n        \"final_rank_score\": fused[\"final_rank_score\"].astype(np.float32),\n    }\n\"\"\")\n\ntrain_attribution_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport yaml\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.attribution import (\n    attribute_one_record,\n    build_dataset_cache,\n    build_fold_signal_catalog,\n    build_repertoire_table_cache,\n    build_bag_records_from_meta,\n    train_mil_one_fold,\n)\nfrom src.utils import ensure_dir, seed_everything, dataset_id_from_name\n\n\ndef safe_auc(y_true, y_prob):\n    from sklearn.metrics import roc_auc_score\n    y_true = np.asarray(y_true)\n    y_prob = np.asarray(y_prob)\n    if len(np.unique(y_true)) < 2:\n        return np.nan\n    return float(roc_auc_score(y_true, y_prob))\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\" / \"attribution.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    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\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    train_meta_all = canonical[canonical[\"source\"] == \"train\"].copy()\n    train_meta_all = train_meta_all[pd.notna(train_meta_all[\"label_positive\"])].copy()\n\n    dataset_names = sorted(train_meta_all[\"dataset_name\"].unique().tolist())\n    seeds = list(cfg_branch[\"training\"][\"seeds\"])\n\n    cfg_dict = cfg_branch[\"dictionary\"]\n    cfg_bag = cfg_branch[\"bag\"]\n    cfg_model = cfg_branch[\"model\"]\n    cfg_train = cfg_branch[\"training\"]\n    cfg_attr = cfg_branch[\"attribution\"]\n\n    all_oof = []\n    all_summary = []\n\n    print(\"=\" * 80)\n    print(\"PHASE 6: Explicit sequence attribution head\")\n    print(\"=\" * 80)\n    print(\"Device:\", device)\n    print(\"Datasets:\", dataset_names)\n\n    for ds_name in dataset_names:\n        ds_id = dataset_id_from_name(ds_name)\n        ds_path = train_root / ds_name\n\n        print(f\"\\\\n{'-' * 80}\")\n        print(f\"Attribution 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        signal_cache = build_dataset_cache(\n            meta_ds=meta_ds,\n            ds_path=ds_path,\n            max_sequences_per_file=cfg_dict[\"max_sequences_per_file\"],\n            random_state=runtime[\"random_state\"],\n            n_jobs=runtime[\"n_jobs\"],\n        )\n\n        table_cache = build_repertoire_table_cache(\n            meta_ds=meta_ds,\n            ds_path=ds_path,\n            max_sequences_per_file=cfg_dict[\"max_sequences_per_file\"],\n            random_state=runtime[\"random_state\"],\n        )\n\n        unique_folds = sorted(meta_ds[\"fold\"].dropna().unique().tolist())\n        ds_seed_frames = []\n        ds_summary_rows = []\n        ds_ranked_rows = []\n        ds_map_rows = []\n\n        for seed in seeds:\n            pred_store = {\n                \"ID\": meta_ds[\"repertoire_id\"].astype(str).tolist(),\n                \"dataset\": meta_ds[\"dataset_name\"].astype(str).tolist(),\n                \"label_positive\": meta_ds[\"label_positive\"].astype(int).tolist(),\n                \"fold\": meta_ds[\"fold\"].tolist(),\n                \"seed\": [seed] * len(meta_ds),\n                \"MIL_ATTR_PROB\": [np.nan] * len(meta_ds),\n            }\n\n            for fold in unique_folds:\n                tr_meta = meta_ds[meta_ds[\"fold\"] != fold].copy().reset_index(drop=True)\n                va_meta = meta_ds[meta_ds[\"fold\"] == fold].copy().reset_index(drop=True)\n                va_global_idx = meta_ds.index[meta_ds[\"fold\"] == fold].to_numpy()\n\n                bundle, _, _ = build_fold_signal_catalog(\n                    fold_train_meta=tr_meta,\n                    cache=signal_cache,\n                    min_freq=cfg_dict[\"min_freq\"],\n                    enrichment=cfg_dict[\"enrichment\"],\n                    top_exact=cfg_dict[\"top_exact\"],\n                    top_clusters=cfg_dict[\"top_clusters\"],\n                    max_repertoires_per_class=cfg_dict[\"max_repertoires_per_class\"],\n                    random_state=seed,\n                )\n\n                train_records = build_bag_records_from_meta(\n                    meta_df=tr_meta,\n                    table_cache=table_cache,\n                    bundle=bundle,\n                    bag_cfg=cfg_bag,\n                    random_state=seed,\n                )\n                val_records = build_bag_records_from_meta(\n                    meta_df=va_meta,\n                    table_cache=table_cache,\n                    bundle=bundle,\n                    bag_cfg=cfg_bag,\n                    random_state=seed + 1000,\n                )\n\n                model, val_auc, val_probs, best_epoch = train_mil_one_fold(\n                    train_records=train_records,\n                    val_records=val_records,\n                    cfg_model=cfg_model,\n                    cfg_train=cfg_train,\n                    device=device,\n                    seed=seed,\n                )\n\n                for idx, p in zip(va_global_idx, val_probs):\n                    pred_store[\"MIL_ATTR_PROB\"][idx] = float(p)\n\n                ds_summary_rows.append({\n                    \"dataset\": ds_name,\n                    \"seed\": seed,\n                    \"fold\": fold,\n                    \"model\": \"MIL_ATTR\",\n                    \"auc\": val_auc,\n                    \"best_epoch\": best_epoch,\n                })\n\n                for rec in val_records:\n                    rows, maps = attribute_one_record(\n                        model=model,\n                        record=rec,\n                        bundle=bundle,\n                        device=device,\n                        ig_steps=cfg_attr[\"ig_steps\"],\n                        max_loo_sequences=cfg_attr[\"max_loo_sequences\"],\n                        rank_weights=cfg_attr[\"rank_weights\"],\n                    )\n                    for row in rows:\n                        row[\"seed\"] = seed\n                        row[\"fold\"] = fold\n                    ds_ranked_rows.extend(rows)\n\n                    maps[\"seed\"] = seed\n                    maps[\"fold\"] = fold\n                    ds_map_rows.append(maps)\n\n            seed_df = pd.DataFrame(pred_store)\n            ds_seed_frames.append(seed_df)\n\n            ds_summary_rows.append({\n                \"dataset\": ds_name,\n                \"seed\": seed,\n                \"fold\": -1,\n                \"model\": \"MIL_ATTR\",\n                \"auc\": safe_auc(seed_df[\"label_positive\"].values, seed_df[\"MIL_ATTR_PROB\"].values),\n                \"best_epoch\": -1,\n            })\n\n        ds_oof = pd.concat(ds_seed_frames, ignore_index=True)\n        ds_oof.to_csv(out_dir / f\"phase6_attribution_oof_{ds_name}.csv\", index=False)\n        all_oof.append(ds_oof)\n\n        ranked_df = pd.DataFrame(ds_ranked_rows)\n        if len(ranked_df):\n            ranked_df = ranked_df.sort_values([\"ID\", \"seed\", \"fold\", \"rank\"], ascending=[True, True, True, True]).reset_index(drop=True)\n            try:\n                ranked_df.to_parquet(out_dir / f\"ranked_sequence_attribution_{ds_name}.parquet\", index=False)\n            except Exception:\n                ranked_df.to_csv(out_dir / f\"ranked_sequence_attribution_{ds_name}_fallback.csv\", index=False)\n\n        if len(ds_map_rows):\n            np.savez_compressed(\n                out_dir / f\"attribution_maps_{ds_name}.npz\",\n                repertoire_ids=np.array([x[\"ID\"] for x in ds_map_rows], dtype=object),\n                seeds=np.array([x[\"seed\"] for x in ds_map_rows], dtype=np.int64),\n                folds=np.array([x[\"fold\"] for x in ds_map_rows], dtype=np.int64),\n                attention=np.vstack([x[\"attention\"] for x in ds_map_rows]).astype(np.float32),\n                loo_delta=np.vstack([x[\"loo_delta\"] for x in ds_map_rows]).astype(np.float32),\n                grad_score=np.vstack([x[\"grad_score\"] for x in ds_map_rows]).astype(np.float32),\n                public_score=np.vstack([x[\"public_score\"] for x in ds_map_rows]).astype(np.float32),\n                cluster_score=np.vstack([x[\"cluster_score\"] for x in ds_map_rows]).astype(np.float32),\n                final_rank_score=np.vstack([x[\"final_rank_score\"] for x in ds_map_rows]).astype(np.float32),\n                mask=np.vstack([x[\"mask\"] for x in ds_map_rows]).astype(np.float32),\n            )\n\n        summary_df = pd.DataFrame(ds_summary_rows)\n        if len(summary_df):\n            summary_df.to_csv(out_dir / f\"phase6_attribution_summary_{ds_name}.csv\", index=False)\n\n            overall = (\n                summary_df[summary_df[\"fold\"] >= 0]\n                .groupby([\"dataset\", \"seed\", \"model\"], as_index=False)\n                .agg(mean_auc=(\"auc\", \"mean\"), std_auc=(\"auc\", \"std\"), max_auc=(\"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\n        print(f\"Saved ranked attribution -> {out_dir / f'ranked_sequence_attribution_{ds_name}.parquet'}\")\n        print(f\"Saved attribution maps -> {out_dir / f'attribution_maps_{ds_name}.npz'}\")\n\n    final_oof = pd.concat(all_oof, ignore_index=True) if len(all_oof) else pd.DataFrame()\n    final_oof.to_csv(out_dir / \"phase6_attribution_oof.csv\", index=False)\n\n    print(\"\\\\n\" + \"=\" * 80)\n    print(\"PHASE 6 DONE\")\n    print(\"Saved:\")\n    print(out_dir / \"phase6_attribution_oof.csv\")\n    print(\"=\" * 80)\n\n\nif __name__ == \"__main__\":\n    main()\n\"\"\")\n\n(PROJECT_ROOT / \"src\" / \"attribution.py\").write_text(attribution_py, encoding=\"utf-8\")\n(PROJECT_ROOT / \"train_attribution.py\").write_text(train_attribution_py, encoding=\"utf-8\")\n\nprint(\"Written:\")\nprint(\"-\", PROJECT_ROOT / \"src\" / \"attribution.py\")\nprint(\"-\", PROJECT_ROOT / \"train_attribution.py\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:21:27.251817Z","iopub.execute_input":"2026-07-24T11:21:27.252358Z","iopub.status.idle":"2026-07-24T11:21:27.269665Z","shell.execute_reply.started":"2026-07-24T11:21:27.252333Z","shell.execute_reply":"2026-07-24T11:21:27.268888Z"}},"outputs":[],"execution_count":null},{"id":"553c1c8a","cell_type":"code","source":"#Cell 3 — imports verify করো\nfrom pathlib import Path\np = Path(\"/kaggle/working/project/src\")\nprint(\"src/ exists:\", p.exists())\nprint(\"contents:\", sorted(f.name for f in p.glob(\"*\")) if p.exists() else \"N/A\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:21:27.271056Z","iopub.execute_input":"2026-07-24T11:21:27.271407Z","iopub.status.idle":"2026-07-24T11:21:27.296265Z","shell.execute_reply.started":"2026-07-24T11:21:27.271385Z","shell.execute_reply":"2026-07-24T11:21:27.295375Z"}},"outputs":[],"execution_count":null},{"id":"c7cbb5c0","cell_type":"code","source":"#Cell 4 — Phase 6 run করো\n!python /kaggle/working/project/train_attribution.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T11:21:27.297363Z","iopub.execute_input":"2026-07-24T11:21:27.297912Z","iopub.status.idle":"2026-07-24T11:21:31.389978Z","shell.execute_reply.started":"2026-07-24T11:21:27.297880Z","shell.execute_reply":"2026-07-24T11:21:31.389272Z"}},"outputs":[],"execution_count":null},{"id":"7468e3e4","cell_type":"code","source":"#Cell 5 — outputs inspect করো\n#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:21:31.391276Z","iopub.execute_input":"2026-07-24T11:21:31.391714Z","iopub.status.idle":"2026-07-24T11:21:31.401269Z","shell.execute_reply.started":"2026-07-24T11:21:31.391682Z","shell.execute_reply":"2026-07-24T11:21:31.400648Z"}},"outputs":[],"execution_count":null}]}