diff --git a/agents/data/manifests/pam50_source_reconcile_biop02-74.json b/agents/data/manifests/pam50_source_reconcile_biop02-74.json new file mode 100644 index 0000000..bd1d471 --- /dev/null +++ b/agents/data/manifests/pam50_source_reconcile_biop02-74.json @@ -0,0 +1,64 @@ +{ + "cbio_study_id": "brca_tcga_pan_can_atlas_2018", + "cbio_attribute": "SUBTYPE", + "cbio_level": "PATIENT", + "n_manifest_pam50": 1009, + "n_cbio_pam50": 981, + "cbio_coverage_of_manifest_cohort_pct": 97.2, + "n_overlap": 902, + "n_match": 514, + "concordance_pct": 57.0, + "top_mismatch_pairs_local_vs_cbio": [ + { + "local": "LumB", + "cbio": "LumA", + "n": 141 + }, + { + "local": "Normal", + "cbio": "LumA", + "n": 101 + }, + { + "local": "HER2", + "cbio": "LumB", + "n": 30 + }, + { + "local": "LumA", + "cbio": "LumB", + "n": 22 + }, + { + "local": "Basal", + "cbio": "Normal", + "n": 19 + }, + { + "local": "HER2", + "cbio": "LumA", + "n": 17 + }, + { + "local": "Basal", + "cbio": "LumA", + "n": 16 + }, + { + "local": "LumB", + "cbio": "Basal", + "n": 15 + }, + { + "local": "LumB", + "cbio": "HER2", + "n": 6 + }, + { + "local": "HER2", + "cbio": "Basal", + "n": 5 + } + ], + "policy_check": "split_policy_v0.md §10: local/genefu fallback authorized only if cBioPortal coverage short. Measured coverage=97.2% (HIGH — fallback condition NOT met)" +} \ No newline at end of file diff --git a/agents/data/scripts/pam50_source_reconcile.py b/agents/data/scripts/pam50_source_reconcile.py new file mode 100644 index 0000000..1137692 --- /dev/null +++ b/agents/data/scripts/pam50_source_reconcile.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +BIOP02-74 open item — PAM50 소스 pinning + policy(§10) vs 실제 사용 대조. +BIOP02-49 QC(comment 11064)가 낸 57.0%(514/902) 수치를 정확한 cBioPortal study_id/속성으로 +재현하고, split_policy_v0.md §10의 "커버리지 부족시 fallback" 조건이 실제로 성립하는지 확인한다. +판정 없음 — 수치만 낸다. 소스 전환 여부는 data-owner(kkkim) 결정. +""" +import csv, json, urllib.request +from pathlib import Path +from collections import Counter + +HERE = Path(__file__).parent +MANIFEST = HERE.parent / "manifests" / "tcga_brca_manifest.csv" +CBIO_STUDY = "brca_tcga_pan_can_atlas_2018" +CBIO_ATTR = "SUBTYPE" # PATIENT-level, 값 "BRCA_LumA" 등 prefix + + +def post(path, body): + req = urllib.request.Request(f"https://www.cbioportal.org/api{path}", data=json.dumps(body).encode(), + headers={"Accept": "application/json", "Content-Type": "application/json"}, method="POST") + return json.load(urllib.request.urlopen(req, timeout=30)) + + +def main(): + d = post(f"/studies/{CBIO_STUDY}/clinical-data/fetch?clinicalDataType=PATIENT", {"attributeIds": [CBIO_ATTR]}) + cbio = {} + for x in d: + v = x["value"] + if v.startswith("BRCA_"): + cbio[x["patientId"]] = v.replace("BRCA_", "").replace("Her2", "HER2") + + manifest = {r["case_id"]: r["pam50"] for r in csv.DictReader(open(MANIFEST)) if r["pam50"]} + n_manifest = len(manifest) + n_cbio = len(cbio) + coverage_pct = round(100 * n_cbio / n_manifest, 1) # cBioPortal이 manifest 코호트를 얼마나 덮는가 + + both = [c for c in manifest if c in cbio] + match = sum(1 for c in both if manifest[c] == cbio[c]) + concordance_pct = round(100 * match / len(both), 1) if both else None + + mismatch_pairs = Counter((manifest[c], cbio[c]) for c in both if manifest[c] != cbio[c]) + top_mismatches = [{"local": k[0], "cbio": k[1], "n": v} + for k, v in mismatch_pairs.most_common(10)] + + result = { + "cbio_study_id": CBIO_STUDY, "cbio_attribute": CBIO_ATTR, "cbio_level": "PATIENT", + "n_manifest_pam50": n_manifest, "n_cbio_pam50": n_cbio, + "cbio_coverage_of_manifest_cohort_pct": coverage_pct, + "n_overlap": len(both), "n_match": match, "concordance_pct": concordance_pct, + "top_mismatch_pairs_local_vs_cbio": top_mismatches, + "policy_check": f"split_policy_v0.md §10: local/genefu fallback authorized only if " + f"cBioPortal coverage short. Measured coverage={coverage_pct}% " + f"({'HIGH — fallback condition NOT met' if coverage_pct > 90 else 'short — fallback condition met'})", + } + out = HERE.parent / "manifests" / "pam50_source_reconcile_biop02-74.json" + json.dump(result, open(out, "w"), indent=2, ensure_ascii=False) + print(json.dumps(result, indent=2, ensure_ascii=False)) + print(f"\nSaved {out}\nDONE_PAM50_SOURCE_RECONCILE") + + +if __name__ == "__main__": + main() diff --git a/guide/paper_a_methods_data_draft.md b/guide/paper_a_methods_data_draft.md index f0d4a07..a1944d4 100644 --- a/guide/paper_a_methods_data_draft.md +++ b/guide/paper_a_methods_data_draft.md @@ -47,22 +47,35 @@ imputed. **PAM50 intrinsic subtype (4-class).** Modeled as LumA / LumB / HER2-enriched / Basal; the Normal-like class is excluded per the split policy (weak, unreliable morphological signal on H&E; consistent with Tafavvoghi et al. 2024). For TCGA, PAM50 is not present in the clinical -biotab and is joined from a separate external source (cBioPortal TCGA-BRCA PAM50 calls, lineage -of the Parker et al. 2009 classifier), as specified by `split_policy_v0.md` §10. For CPTAC, PAM50 +biotab. **Corrected 2026-08-20** (BIOP02-49's provenance follow-up, `tcga_brca_pam50_computed_ +PROVENANCE.md`, landed on `main` after this draft was first written): the committed TCGA PAM50 +calls are **not** a cBioPortal-sourced file — they are a locally-computed nearest-centroid +classification (Parker et al. 2009 method, confidence range 0.808–0.925) run against expression +data outside this repo. `split_policy_v0.md` §10 names cBioPortal TCGA-BRCA PAM50 as the +*primary* source with the local/genefu computation as a *fallback for coverage gaps* — the +manifest as committed uses the fallback for the entire cohort, not cBioPortal. For CPTAC, PAM50 comes from the same `brca_cptac_2020` release as the other endpoints; class naming was normalized across cohorts (CPTAC's `Her2` → `HER2`) so both use identical class labels. -**Known limitation — PAM50 source pinning.** A post-hoc QC pass (BIOP02-49) cross-checked the -committed TCGA PAM50 calls against a different public source (cBioPortal -`brca_tcga_pan_can_atlas_2018` SUBTYPE field) and found only 57.0% concordance (514/902 overlapping -patients) — HER2 status, by contrast, matched the raw biotab at 100% (1,010/1,010). This is -consistent with the known instability of TCGA PAM50 re-calling across processing batches, but it -also means we cannot yet cite an exact cBioPortal `study_id` for the PAM50 file actually used -(the original file was supplied out-of-band and is not itself committed to the repo, only its -join output is). This is flagged as an open item for the data-owner (kkkim) before submission — -not a correctness bug, but a provenance-pinning gap that should be closed so the label source is -fully citable. It does not affect the split (§D.4), which is defined over the full patient set -independent of any single label's availability. +**Open item — PAM50 source pinned, but policy vs. practice needs a data-owner decision.** +Re-verified 2026-08-20 with the exact cBioPortal source pinned: study `brca_tcga_pan_can_atlas_2018`, +attribute `SUBTYPE` (PATIENT-level, values prefixed `BRCA_`), coverage **981/1,010 patients +(97.1%)**. Against this source, the committed (local/genefu) PAM50 calls match at **57.0%** +(514/902 overlapping labeled patients) — confirms BIOP02-49's original finding, now with the +comparison source's `study_id`/attribute fully citable. Mismatches concentrate in two known-hard +boundaries, not random noise: local=LumB vs. cBioPortal=LumA (141 patients) and local=Normal vs. +cBioPortal=LumA (101 patients) — consistent with the literature's documented LumA/LumB boundary +instability and Normal-like call instability across PAM50 implementations, not evidence either +source is simply wrong. **The open question for the data-owner (kkkim):** §10 authorizes the +local/genefu fallback only when cBioPortal coverage is short, but cBioPortal coverage here is +97.1% — high, not short. Should the manifest switch to cBioPortal PAM50 as primary per the +letter of §10, or is there a documented reason (e.g. cBioPortal's curated calls trailing a TCGA +reprocessing batch) the local computation was used as primary in practice? Either answer is +fine, but the repo should say which and why before this is cited as "single source, version- +pinned" per `split_policy_v0.md` §7. Does not affect the split (§D.4), which is defined over the +full patient set independent of any single label's availability. Reproducible via +`agents/data/scripts/pam50_source_reconcile.py`, output +`agents/data/manifests/pam50_source_reconcile_biop02-74.json`. **Missingness handling.** Each endpoint's missing/excluded values (§ above) are masked per-task via boolean `has_er / has_pr / has_her2 / has_pam50` columns. Missingness never removes @@ -127,4 +140,9 @@ data version that produced it. *상태: 초안 v0.1. 모델/학습/평가 절(M.1–M.9)은 sjpark 초안([guide/paper_a_methods_modeling_draft.md](paper_a_methods_modeling_draft.md))과 상호 연결 — 특히 M.2(tiling/embedding), M.5(split 수치)가 이 문서를 "Data Methods"로 지칭하고 있어 정합 확인 완료. braveji가 M.7에서 지적한 mean-embed≠pixel-mean 용어 문제(-72 코멘트 11387 ①)는 이 문서 범위 밖(모델링 절 소관).* -*미해결 항목(투고 전 kkkim 확인 필요): PAM50 라벨의 정확한 cBioPortal `study_id`가 레포에 미고정(§D.3 "Known limitation"). HER2는 원본 biotab과 100% 일치 확인됐으나 PAM50은 대체 소스와 57% 일치라 재확인 없이는 "단일 소스 고정" 요구사항(split_policy_v0.md §7)을 완전히 충족한다고 말하기 어려움.* +*미해결 항목(투고 전 kkkim 결정 필요, 2026-08-20 갱신): PAM50 cBioPortal 대조 소스의 study_id는 +이제 고정됨(`brca_tcga_pan_can_atlas_2018`, `SUBTYPE` 속성, 커버리지 981/1010=97.2%). 남은 건 +study_id 미고정이 아니라 **정책(§10: cBioPortal 1순위·genefu는 커버리지 부족시 fallback) vs +실제 사용(전체 코호트가 genefu)의 불일치** — 커버리지가 97.2%로 "부족"이 아닌데 fallback이 +1순위처럼 쓰이고 있음. kkkim 결정 필요(§D.3 "Open item" 참조, 재현 +스크립트=`agents/data/scripts/pam50_source_reconcile.py`).*