{"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":"a352fb23","cell_type":"markdown","source":"# Phase 7 — Mal-ID style Fusion (rank-normalized blending + non-negative logistic stacking)\n\nFuses Branch A + B + C + D OOF predictions via rank-normalized blending and non-negative logistic stacking with dataset-specific weights. Outputs fusion_oof.csv, fusion_weights.csv, fusion_summary.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- Phase 1 notebook output\n- Phase 2 notebook output (branch_a_oof.csv)\n- Phase 3 notebook output (branch_b_oof.csv)\n- Phase 4 notebook output (branch_c_oof.csv)\n- Phase 5 notebook output (branch_d_oof.csv)\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":"d4a4063a","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},"outputs":[],"execution_count":null},{"id":"ab9693ac","cell_type":"code","source":"#Cell 1 — Phase 7 config file write করো\n# Cell 1: Phase 7 fusion config file write\n\nfrom pathlib import Path\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nfusion_yaml = \"\"\"\nenabled: true\n\npaths:\n  output_root: /kaggle/working/project/artifacts/phase7_fusion\n  branch_a_oof: /kaggle/working/project/artifacts/phase2_branch_a/branch_a_oof.csv\n  branch_b_oof: /kaggle/working/project/artifacts/phase3_branch_b/branch_b_oof.csv\n  branch_c_oof: /kaggle/working/project/artifacts/phase4_branch_c/branch_c_oof.csv\n  branch_d_oof: /kaggle/working/project/artifacts/phase5_branch_d/branch_d_oof.csv\n\ntraining:\n  random_state: 42\n\"\"\"\n\n(PROJECT_ROOT / \"configs\" / \"fusion.yaml\").write_text(fusion_yaml.strip() + \"\\n\", encoding=\"utf-8\")\nprint(\"Written:\", PROJECT_ROOT / \"configs\" / \"fusion.yaml\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"0e781598","cell_type":"code","source":"#Cell 2 — fusion code files write করো\n# Cell 2: src/fusion.py and train_fusion.py write\n\nfrom pathlib import Path\nfrom textwrap import dedent\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\n\nfusion_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.linear_model import LogisticRegression\n\n\nMETA_COLS = {\"ID\", \"dataset\", \"label_positive\", \"fold\", \"seed\"}\n\n\ndef safe_auc(y_true, y_prob):\n    from sklearn.metrics import roc_auc_score\n\n    y_true = np.asarray(y_true)\n    y_prob = np.asarray(y_prob)\n\n    valid = ~pd.isna(y_true) & ~pd.isna(y_prob)\n    y_true = y_true[valid]\n    y_prob = y_prob[valid]\n\n    if len(y_true) == 0 or len(np.unique(y_true)) < 2:\n        return np.nan\n    return float(roc_auc_score(y_true, y_prob))\n\n\ndef rank_normalize_array(x: np.ndarray) -> np.ndarray:\n    x = np.asarray(x, dtype=float)\n    out = np.zeros_like(x, dtype=np.float32)\n\n    if len(x) == 0:\n        return out\n\n    valid = ~np.isnan(x)\n    idx = np.where(valid)[0]\n    if len(idx) == 0:\n        return out\n    if len(idx) == 1:\n        out[idx[0]] = 1.0\n        return out\n\n    vals = x[idx]\n    order = np.argsort(-vals, kind=\"mergesort\")\n    for rank_pos, pos in enumerate(order):\n        out[idx[pos]] = float(1.0 - rank_pos / (len(idx) - 1))\n    return out.astype(np.float32)\n\n\ndef rank_normalize_by_dataset(df: pd.DataFrame, pred_cols: List[str]) -> pd.DataFrame:\n    out = df.copy()\n    for ds_name, g in out.groupby(\"dataset\"):\n        idx = g.index\n        for c in pred_cols:\n            out.loc[idx, c] = rank_normalize_array(g[c].values)\n    return out\n\n\ndef _pick_branch_pred_column(df: pd.DataFrame) -> str:\n    preferred = [\"AVG\", \"MIL_PROB\", \"MIL_ATTR_PROB\"]\n    for c in preferred:\n        if c in df.columns:\n            return c\n\n    candidate_cols = [c for c in df.columns if c not in META_COLS]\n    numeric_cols = [c for c in candidate_cols if pd.api.types.is_numeric_dtype(df[c])]\n    if len(numeric_cols) == 0:\n        raise ValueError(\"No prediction column found in OOF file.\")\n    return numeric_cols[-1]\n\n\ndef load_branch_oof(path: str | Path, branch_name: str) -> pd.DataFrame:\n    path = Path(path)\n    if not path.exists():\n        return pd.DataFrame(columns=[\"ID\", \"dataset\", \"label_positive\", \"fold\", f\"{branch_name}_pred\"])\n\n    df = pd.read_csv(path)\n\n    required = [\"ID\", \"dataset\"]\n    for c in required:\n        if c not in df.columns:\n            raise ValueError(f\"{path} missing required column: {c}\")\n\n    pred_col = _pick_branch_pred_column(df)\n\n    keep_cols = [\"ID\", \"dataset\"]\n    if \"label_positive\" in df.columns:\n        keep_cols.append(\"label_positive\")\n    if \"fold\" in df.columns:\n        keep_cols.append(\"fold\")\n    keep_cols.append(pred_col)\n\n    x = df[keep_cols].copy()\n    x = x.rename(columns={pred_col: f\"{branch_name}_pred\"})\n\n    # aggregate across seeds if duplicated\n    group_cols = [\"ID\", \"dataset\"]\n    agg_map = {f\"{branch_name}_pred\": \"mean\"}\n\n    if \"label_positive\" in x.columns:\n        group_cols.append(\"label_positive\")\n    if \"fold\" in x.columns:\n        group_cols.append(\"fold\")\n\n    x = x.groupby(group_cols, as_index=False).agg(agg_map)\n\n    if \"label_positive\" not in x.columns:\n        x[\"label_positive\"] = np.nan\n    if \"fold\" not in x.columns:\n        x[\"fold\"] = np.nan\n\n    return x\n\n\ndef merge_branch_oofs(branch_frames: Dict[str, pd.DataFrame]) -> pd.DataFrame:\n    valid_items = [(k, v.copy()) for k, v in branch_frames.items() if len(v) > 0]\n    if len(valid_items) == 0:\n        return pd.DataFrame(columns=[\"ID\", \"dataset\", \"label_positive\", \"fold\"])\n\n    base_name, base_df = valid_items[0]\n    merged = base_df.copy()\n\n    for name, df in valid_items[1:]:\n        pred_col = f\"{name}_pred\"\n        use_cols = [\"ID\", \"dataset\", pred_col]\n        if pred_col not in df.columns:\n            continue\n        merged = merged.merge(df[use_cols], on=[\"ID\", \"dataset\"], how=\"outer\")\n\n        if \"label_positive\" in df.columns and \"label_positive\" in merged.columns:\n            lab_map = df[[\"ID\", \"dataset\", \"label_positive\"]].drop_duplicates()\n            merged = merged.merge(\n                lab_map.rename(columns={\"label_positive\": \"label_positive_tmp\"}),\n                on=[\"ID\", \"dataset\"],\n                how=\"left\",\n            )\n            merged[\"label_positive\"] = merged[\"label_positive\"].fillna(merged[\"label_positive_tmp\"])\n            merged = merged.drop(columns=[\"label_positive_tmp\"])\n\n        if \"fold\" in df.columns and \"fold\" in merged.columns:\n            fold_map = df[[\"ID\", \"dataset\", \"fold\"]].drop_duplicates()\n            merged = merged.merge(\n                fold_map.rename(columns={\"fold\": \"fold_tmp\"}),\n                on=[\"ID\", \"dataset\"],\n                how=\"left\",\n            )\n            merged[\"fold\"] = merged[\"fold\"].fillna(merged[\"fold_tmp\"])\n            merged = merged.drop(columns=[\"fold_tmp\"])\n\n    if \"label_positive\" not in merged.columns:\n        merged[\"label_positive\"] = np.nan\n    if \"fold\" not in merged.columns:\n        merged[\"fold\"] = np.nan\n\n    pred_cols = [c for c in merged.columns if c.endswith(\"_pred\")]\n\n    for c in pred_cols:\n        if merged[c].isna().all():\n            merged[c] = 0.5\n        else:\n            merged[c] = merged[c].fillna(merged[c].mean())\n\n    merged = merged.sort_values([\"dataset\", \"ID\"]).reset_index(drop=True)\n    return merged\n\n\ndef simple_mean_pred(df: pd.DataFrame, pred_cols: List[str]) -> np.ndarray:\n    return df[pred_cols].mean(axis=1).values.astype(float)\n\n\ndef rank_mean_pred(df: pd.DataFrame, pred_cols: List[str]) -> np.ndarray:\n    tmp = rank_normalize_by_dataset(df[[\"dataset\"] + pred_cols].copy(), pred_cols)\n    return tmp[pred_cols].mean(axis=1).values.astype(float)\n\n\ndef fit_nonnegative_weights(X: np.ndarray, y: np.ndarray, random_state: int = 42) -> np.ndarray:\n    X = np.asarray(X, dtype=float)\n    y = np.asarray(y, dtype=int)\n\n    valid = ~np.isnan(X).any(axis=1) & ~np.isnan(y)\n    X = X[valid]\n    y = y[valid]\n\n    if len(X) == 0 or X.shape[1] == 0 or len(np.unique(y)) < 2:\n        w = np.ones(X.shape[1], dtype=float) / max(1, X.shape[1])\n        return w\n\n    clf = LogisticRegression(max_iter=4000, random_state=random_state)\n    clf.fit(X, y)\n\n    coef = np.clip(np.asarray(clf.coef_[0], dtype=float), 0, None)\n    if coef.sum() <= 0:\n        coef = np.ones_like(coef, dtype=float)\n    coef = coef / coef.sum()\n    return coef.astype(float)\n\n\ndef global_stack_oof(df: pd.DataFrame, pred_cols: List[str], random_state: int = 42) -> Tuple[np.ndarray, np.ndarray]:\n    out = np.zeros(len(df), dtype=float)\n    weights_per_fold = []\n\n    usable = df.dropna(subset=[\"label_positive\"]).copy()\n    usable[\"label_positive\"] = usable[\"label_positive\"].astype(int)\n\n    folds = sorted([f for f in usable[\"fold\"].dropna().unique().tolist()])\n    if len(folds) == 0:\n        w = fit_nonnegative_weights(usable[pred_cols].values, usable[\"label_positive\"].values, random_state=random_state)\n        out[:] = np.dot(df[pred_cols].values, w)\n        return out, w\n\n    for fold in folds:\n        tr = usable[usable[\"fold\"] != fold].copy()\n        va_idx = usable.index[usable[\"fold\"] == fold].to_numpy()\n\n        if len(tr) == 0 or len(va_idx) == 0:\n            continue\n\n        w = fit_nonnegative_weights(tr[pred_cols].values, tr[\"label_positive\"].values, random_state=random_state)\n        weights_per_fold.append(w)\n        out[va_idx] = np.dot(df.loc[va_idx, pred_cols].values, w)\n\n    if len(weights_per_fold) == 0:\n        full_w = fit_nonnegative_weights(usable[pred_cols].values, usable[\"label_positive\"].values, random_state=random_state)\n        out[:] = np.dot(df[pred_cols].values, full_w)\n        return out, full_w\n\n    full_w = fit_nonnegative_weights(usable[pred_cols].values, usable[\"label_positive\"].values, random_state=random_state)\n    return out, full_w\n\n\ndef dataset_stack_oof(df: pd.DataFrame, pred_cols: List[str], random_state: int = 42) -> Tuple[np.ndarray, pd.DataFrame]:\n    out = np.zeros(len(df), dtype=float)\n    weight_rows = []\n\n    usable = df.dropna(subset=[\"label_positive\"]).copy()\n    usable[\"label_positive\"] = usable[\"label_positive\"].astype(int)\n\n    for ds_name, g in usable.groupby(\"dataset\"):\n        folds = sorted([f for f in g[\"fold\"].dropna().unique().tolist()])\n        if len(folds) == 0:\n            w = fit_nonnegative_weights(g[pred_cols].values, g[\"label_positive\"].values, random_state=random_state)\n            out[g.index] = np.dot(df.loc[g.index, pred_cols].values, w)\n        else:\n            for fold in folds:\n                tr = g[g[\"fold\"] != fold].copy()\n                va_idx = g.index[g[\"fold\"] == fold].to_numpy()\n                if len(tr) == 0 or len(va_idx) == 0:\n                    continue\n                w = fit_nonnegative_weights(tr[pred_cols].values, tr[\"label_positive\"].values, random_state=random_state)\n                out[va_idx] = np.dot(df.loc[va_idx, pred_cols].values, w)\n\n        full_w = fit_nonnegative_weights(g[pred_cols].values, g[\"label_positive\"].values, random_state=random_state)\n        for c, wv in zip(pred_cols, full_w):\n            weight_rows.append({\n                \"scope\": \"dataset\",\n                \"dataset\": ds_name,\n                \"method\": \"STACK_DATASET\",\n                \"branch_feature\": c,\n                \"weight\": float(wv),\n            })\n\n    weights_df = pd.DataFrame(weight_rows)\n    return out, weights_df\n\n\ndef build_fusion_outputs(merged_df: pd.DataFrame, random_state: int = 42):\n    df = merged_df.copy()\n    pred_cols = [c for c in df.columns if c.endswith(\"_pred\")]\n\n    if len(pred_cols) == 0:\n        raise ValueError(\"No branch prediction columns found for fusion.\")\n\n    df[\"SIMPLE_MEAN\"] = simple_mean_pred(df, pred_cols)\n    df[\"RANK_MEAN\"] = rank_mean_pred(df, pred_cols)\n\n    global_oof, global_w = global_stack_oof(df, pred_cols, random_state=random_state)\n    df[\"STACK_GLOBAL\"] = global_oof\n\n    dataset_oof, dataset_weights_df = dataset_stack_oof(df, pred_cols, random_state=random_state)\n    df[\"STACK_DATASET\"] = dataset_oof\n\n    global_weights_df = pd.DataFrame({\n        \"scope\": [\"global\"] * len(pred_cols),\n        \"dataset\": [\"ALL\"] * len(pred_cols),\n        \"method\": [\"STACK_GLOBAL\"] * len(pred_cols),\n        \"branch_feature\": pred_cols,\n        \"weight\": global_w.astype(float),\n    })\n\n    weights_df = pd.concat([global_weights_df, dataset_weights_df], ignore_index=True)\n\n    summary_rows = []\n    methods = [\"SIMPLE_MEAN\", \"RANK_MEAN\", \"STACK_GLOBAL\", \"STACK_DATASET\"]\n\n    usable = df.dropna(subset=[\"label_positive\"]).copy()\n    usable[\"label_positive\"] = usable[\"label_positive\"].astype(int)\n\n    for ds_name, g in usable.groupby(\"dataset\"):\n        for m in methods:\n            summary_rows.append({\n                \"dataset\": ds_name,\n                \"method\": m,\n                \"auc\": safe_auc(g[\"label_positive\"].values, g[m].values),\n                \"n_samples\": int(len(g)),\n            })\n\n    for m in methods:\n        summary_rows.append({\n            \"dataset\": \"ALL\",\n            \"method\": m,\n            \"auc\": safe_auc(usable[\"label_positive\"].values, usable[m].values),\n            \"n_samples\": int(len(usable)),\n        })\n\n    summary_df = pd.DataFrame(summary_rows).sort_values([\"dataset\", \"auc\"], ascending=[True, False]).reset_index(drop=True)\n    return df, weights_df, summary_df\n\"\"\")\n\ntrain_fusion_py = dedent(\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\nimport pandas as pd\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.fusion import (\n    build_fusion_outputs,\n    load_branch_oof,\n    merge_branch_oofs,\n)\nfrom src.utils import ensure_dir\n\n\ndef main():\n    cfg = yaml.safe_load((PROJECT_ROOT / \"configs\" / \"fusion.yaml\").read_text())\n    out_dir = ensure_dir(cfg[\"paths\"][\"output_root\"])\n    random_state = cfg[\"training\"][\"random_state\"]\n\n    branch_paths = {\n        \"branch_a\": cfg[\"paths\"][\"branch_a_oof\"],\n        \"branch_b\": cfg[\"paths\"][\"branch_b_oof\"],\n        \"branch_c\": cfg[\"paths\"][\"branch_c_oof\"],\n        \"branch_d\": cfg[\"paths\"][\"branch_d_oof\"],\n    }\n\n    print(\"=\" * 80)\n    print(\"PHASE 7: Final fusion layer\")\n    print(\"=\" * 80)\n\n    branch_frames = {}\n    for name, path in branch_paths.items():\n        df = load_branch_oof(path, branch_name=name)\n        branch_frames[name] = df\n        print(f\"{name}: rows={len(df)} | path={path}\")\n\n    merged = merge_branch_oofs(branch_frames)\n    print(\"\\\\nMerged fusion table shape:\", merged.shape)\n    print(\"Columns:\", merged.columns.tolist())\n\n    fusion_oof, weights_df, summary_df = build_fusion_outputs(merged, random_state=random_state)\n\n    fusion_oof.to_csv(out_dir / \"fusion_oof.csv\", index=False)\n    weights_df.to_csv(out_dir / \"fusion_weights.csv\", index=False)\n    summary_df.to_csv(out_dir / \"fusion_summary.csv\", index=False)\n\n    print(\"\\\\nFusion summary:\")\n    print(summary_df.to_string(index=False))\n\n    print(\"\\\\nSaved:\")\n    print(out_dir / \"fusion_oof.csv\")\n    print(out_dir / \"fusion_weights.csv\")\n    print(out_dir / \"fusion_summary.csv\")\n    print(\"=\" * 80)\n\n\nif __name__ == \"__main__\":\n    main()\n\"\"\")\n\n(PROJECT_ROOT / \"src\" / \"fusion.py\").write_text(fusion_py, encoding=\"utf-8\")\n(PROJECT_ROOT / \"train_fusion.py\").write_text(train_fusion_py, encoding=\"utf-8\")\n\nprint(\"Written:\")\nprint(\"-\", PROJECT_ROOT / \"src\" / \"fusion.py\")\nprint(\"-\", PROJECT_ROOT / \"train_fusion.py\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"65b8b88b","cell_type":"code","source":"#Cell 3 — imports verify করো\n# Cell 3: Fusion imports verify\n\nimport sys\nfrom pathlib import Path\n\nPROJECT_ROOT = Path(\"/kaggle/working/project\")\nif str(PROJECT_ROOT) not in sys.path:\n    sys.path.insert(0, str(PROJECT_ROOT))\n\nfrom src.fusion import (\n    load_branch_oof,\n    merge_branch_oofs,\n    build_fusion_outputs,\n)\n\nprint(\"Phase 7 imports OK\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"381784d5","cell_type":"code","source":"# Cell 4: Run Phase 7 fusion training on OOF predictions\n\n!python /kaggle/working/project/train_fusion.py","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"ac9dc36e","cell_type":"code","source":"# Cell 5: Inspect fusion outputs\n\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},"outputs":[],"execution_count":null}]}