From 0134da7238574ab0951c432f300d178af9f70715 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 20 Aug 2026 17:54:19 +0900 Subject: [PATCH] =?UTF-8?q?BIOP02-148:=20Table=201=20=EC=BD=94=ED=98=B8?= =?UTF-8?q?=ED=8A=B8=20=ED=8A=B9=EC=84=B1=ED=91=9C=205=EC=95=94=EC=A2=85?= =?UTF-8?q?=20+=20participant=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5암종(유방·폐·대장·위·두경부) 환자수/슬라이드수/train-holdout/나이/성별/ stage/grade/TSS site수 + 엔드포인트별 유병률/결측/assay source. 전부 기존 정본 파일(patient_labels.csv/split.csv/manifest/mil_cost_results.json) 집계, 신규 라벨 없음. grade 커버리지 유방·폐·대장 0%(cBioPortal 속성 자체 부재, 실측 확인) vs 위·두경부 ~100% 그대로 노출. check_table1_numbers.py로 자체 검증: holdout n_pos 전부 mil_cost_results.json 과 1:1 일치 + manuscript/sections/02_results.md R1/R2 인용 수치와도 불일치 0건(DoD "분모 불일치 0건" 항목). Table 1 배치는 본문 권장이나 원고 prose는 미수정(리뷰 후 편입 제안). 독립 리뷰 요청 대상: 박세진(sezinie000) 또는 지용기, DoD 명시. --- .../crosscancer/table1/BIOP02-148_FINDINGS.md | 82 +++++ .../crosscancer/table1/PARTICIPANT_FLOW.md | 13 + experiments/crosscancer/table1/TABLE1.md | 31 ++ .../crosscancer/table1/build_table1.py | 183 ++++++++++ .../table1/check_table1_numbers.py | 73 ++++ .../table1/fetch_table1_clinical.py | 121 +++++++ .../crosscancer/table1/table1_clinical.json | 143 ++++++++ .../crosscancer/table1/table1_master.json | 323 ++++++++++++++++++ 8 files changed, 969 insertions(+) create mode 100644 experiments/crosscancer/table1/BIOP02-148_FINDINGS.md create mode 100644 experiments/crosscancer/table1/PARTICIPANT_FLOW.md create mode 100644 experiments/crosscancer/table1/TABLE1.md create mode 100644 experiments/crosscancer/table1/build_table1.py create mode 100644 experiments/crosscancer/table1/check_table1_numbers.py create mode 100644 experiments/crosscancer/table1/fetch_table1_clinical.py create mode 100644 experiments/crosscancer/table1/table1_clinical.json create mode 100644 experiments/crosscancer/table1/table1_master.json diff --git a/experiments/crosscancer/table1/BIOP02-148_FINDINGS.md b/experiments/crosscancer/table1/BIOP02-148_FINDINGS.md new file mode 100644 index 0000000..8c3036e --- /dev/null +++ b/experiments/crosscancer/table1/BIOP02-148_FINDINGS.md @@ -0,0 +1,82 @@ +# BIOP02-148 — Table 1(코호트 특성표) 5암종 + +`claim_level: descriptive`(가설 검정 아님, 사전등록 대상 없음) · jamie, 2026-08-20 + +## 범위 + +티켓 본문 그대로 전체 범위(축소 옵션 미사용) — 5암종(유방·폐·대장·위·두경부) × 환자수/ +슬라이드수/train·holdout/나이/성별/stage/grade/tissue source site 수 + 엔드포인트별 +유병률·결측·assay source + participant flow. 신규 모델링·통계 검정 없음 — **전부 이미 커밋된 +정본 파일에서 집계만** 했다(가설 검정이 아니므로 결과-보기-전 사전등록은 생략, 대신 "숫자는 +전부 기존 파일 인용, 재계산 없음"을 아래 산출물 자체에 명시). + +## 산출물 + +- `fetch_table1_clinical.py` → `table1_clinical.json` — age/sex/stage/grade/TSS site + (cBioPortal PATIENT+SAMPLE level, 5개 study). **정정 1건**: GRADE·TISSUE_SOURCE_SITE_CODE는 + PATIENT-level이 아니라 SAMPLE-level 속성임을 최초 실행에서 0건으로 확인 후 발견(팀 기존 + `sh_fetch_labels.py`가 이미 같은 패턴 사용 — 뒤늦게 확인). 재실행 후 정상 값 확보. +- `build_table1.py` → `table1_master.json`(정본) + `TABLE1.md` + `PARTICIPANT_FLOW.md` — + 기존 `patient_labels.csv`/`split.csv`/`tcga_brca_manifest.csv`/`mil_cost_results.json`에서 + 집계, 신규 라벨 없음. +- `check_table1_numbers.py` — DoD "결과 파일과 1:1 대조" 자체 검증(아래 결과 참조). + +## 핵심 발견 — 결과 파일과의 대조 + +`check_table1_numbers.py` 실행 결과 **불일치 0건**: +- Table 1b의 holdout n_pos·n_holdout 6개 암종×엔드포인트 전부 `mil_cost_results.json`(정본)과 + 정확히 일치. +- **`manuscript/sections/02_results.md`의 R2 표(축별 홀드아웃 양성 표본 6행)와 R1 prose가 + 인용한 헤드라인 n_pos(폐 LUSC 153·두경부 HPV 26·두경부 grade 41) 전부 Table 1과 1:1 + 일치** — 이게 DoD의 "본문·표 R1/R2와 분모 불일치 0건" 항목이다. 원고가 이미 실측 수치를 + 쓰고 있었고 이번 Table 1이 그걸 독립적으로 재확인한 셈이다(원고를 보고 베낀 게 아니라 + `mil_cost_results.json`에서 각자 계산 → 우연 일치가 아니라 같은 정본을 공유). + +## 결과 요약 + +Table 1 전문 = `TABLE1.md`, 참여자 흐름 = `PARTICIPANT_FLOW.md`. 주목할 점만: + +1. **grade 커버리지가 암종마다 극단적으로 다르다.** 위(440/440, 100%)·두경부(519/523, 99%)는 + 완비, 유방·폐·대장은 **0%**(cBioPortal `GRADE` 속성 자체가 이 3개 study엔 없음 — 체리피킹 + 아니라 실측 확인, BIOP02-140에서 폐도 같은 걸 이미 확인했음). 이걸 Table 1에 그대로 + 드러내는 게 "결측을 숨기지 않는다"는 프로젝트 원칙에 맞다고 판단해 각주 없이 표 자체에 + 0.0%로 명시했다. +2. **PAM50은 이진 유병률 정의가 안 된다.** 초안에서 "positive=Normal-like 제외"로 억지로 + 이진화했더니 100%가 나와(정의상 당연) 무의미했다 — 5-class 분포(LumB 321·LumA 243· + Basal 198·HER2 120)로 바꿔 표기했다. 이 실수는 결과를 보기 전에 잡았다(집계 스크립트 + 개발 중 이상값으로 발견, 발표용 숫자로 나가기 전). +3. **holdout denominator가 같은 암종 안에서도 엔드포인트마다 다르다**(티켓이 걱정한 + "endpoint denominator audit" 그대로) — 위는 lauren_diffuse n=58 vs msi_h n=107, 같은 + GASTRIC holdout 132명 중 라벨 있는 하위집합만 쓰기 때문. Table 1b에 암종별이 아니라 + 엔드포인트별 행으로 분리해 이 차이가 보이게 했다. +4. **폐 KRAS-G12C 관련 교차확인**: `LUNG_NSCLC/full/SUBTYPE_BASELINE_NOTE.md`(2026-07-14, + 기존 산출물)가 이미 histology-only baseline 0.793(n_pos 15/315)을 봉인해뒀다는 걸 이번에 + 확인했다 — R1 prose의 "조직형만으로 예측한 기준선 0.793"의 정확한 출처다. 방금 병합 + 대기 중인 BIOP02-140(jamie, PR #136)이 이 기준선에 purity/stage/site를 더한 확장판 + (0.802)이라 서로 다른 카드지만 같은 결론(KRAS는 histology 편중)을 독립적으로 강화한다 — + 참고로만 남긴다, BIOP02-148 자체 스코프는 아님. + +## Table 1 배치 판단 (DoD 항목) + +**본문(main text) 권장.** TRIPOD+AI 13b는 baseline 특성표를 본문에 두는 걸 표준으로 삼고, +`02_results.md` R1 앞에 두는 게 자연스럽다(R0가 "다섯 암종 약 열다섯 endpoint"를 서술하는데 +Table 1이 그 숫자의 근거가 됨). 다만 **원고 prose(`02_results.md`/`03_methods.md`) 직접 +수정은 이번 PR에서 안 했다** — 다른 분들이 이미 쓰고 있는 살아있는 원고라 Owner≠Reviewer +취지상 배치·문구는 리뷰에서 합의 후 반영하는 게 맞다고 판단. 지금은 `experiments/crosscancer/ +table1/`에 독립 파일로 두고, 리뷰 통과 후 원고 편입을 별도 커밋으로 제안. + +## DoD 체크 + +- [x] Table 1 초안 (배치 판단 포함 — 위) +- [x] 모든 수치가 커밋된 결과 파일과 1:1 대조 — `check_table1_numbers.py`, 불일치 0건 +- [x] 본문·표 R1/R2와 분모 불일치 0건 — 위 스크립트로 확인(6/6 R2행 + 3개 R1 인용 전부 일치) +- [x] participant flow — `PARTICIPANT_FLOW.md` +- [ ] **독립 리뷰: 박세진(sezinie000) 또는 지용기** — 아직 + +## 스킵/후속 + +- 유방 er/pr/her2/pam50은 holdout(val+test) 분리 없이 코호트 전체로 표기(Table 1b에 명시) — + Paper A/B 코드가 CPTAC 외부검증을 별도로 쓰고 이 매니페스트 자체엔 holdout-전용 예측치가 + 없어서다. 필요하면 후속으로 holdout만 분리한 값을 추가. +- assay-source 텍스트는 각 fetch 스크립트 주석·문서에서 인용(신규 조사 아님) — 표기가 틀리면 + 리뷰에서 정정 부탁. diff --git a/experiments/crosscancer/table1/PARTICIPANT_FLOW.md b/experiments/crosscancer/table1/PARTICIPANT_FLOW.md new file mode 100644 index 0000000..e61ee61 --- /dev/null +++ b/experiments/crosscancer/table1/PARTICIPANT_FLOW.md @@ -0,0 +1,13 @@ +# Participant flow — 5암종 (TRIPOD+AI 13b 짝) + +`n식별` = patient_labels.csv/manifest에 라벨이 조인된 환자 수(=GDC 매니페스트에서 H&E 슬라이드+임상데이터 결합 가능 subset, 이미 필터링된 상태 — raw GDC 전체 대비 제외 사유는 각 코호트 원 매니페스트 문서 참조). 여기서부터 아래로: 라벨 결측 없는 환자 → train/holdout 분리. + +| 암종 | n(라벨 조인, 시작점) | n(슬라이드 보유) | n(train) | n(holdout=val+test) | +|---|---|---|---|---| +| 유방 | 1010 | 1010 | 707 | 303 | +| 폐 | 1050 | 1026 | 735 | 315 | +| 대장 | 534 | 523 | 374 | 160 | +| 위 | 440 | 439 | 308 | 132 | +| 두경부 | 523 | 468 | 366 | 157 | + +엔드포인트별 최종 분석 대상(holdout denominator)은 Table 1b 참조 — 암종 안에서도 엔드포인트마다 라벨 결측 패턴이 달라 holdout n이 다르다(예: 위 lauren_diffuse n=58 vs msi_h n=107, 같은 GASTRIC holdout 132명 중 라벨 있는 하위집합만). diff --git a/experiments/crosscancer/table1/TABLE1.md b/experiments/crosscancer/table1/TABLE1.md new file mode 100644 index 0000000..e3ae20c --- /dev/null +++ b/experiments/crosscancer/table1/TABLE1.md @@ -0,0 +1,31 @@ +# Table 1 — 코호트 baseline 특성 (5암종) + +`claim_level: descriptive` — 사전등록 대상 없음(가설 검정 아님). 모든 수치는 `table1_master.json`(정본)에서 그대로 인용, 이 문서에서 재계산 없음. + +| 암종 | N 환자 | N 슬라이드 | Train/Val/Test | 나이 중앙값(IQR) | 여성 % | Stage I | II | III | IV | missing | Grade 커버리지 | TSS site 수 | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 유방 | 1010 | 1010 | 707/152/151 | 58.5 (49–68, n=996) | 99% | 167 (17%) | 569 (56%) | 227 (22%) | 16 (2%) | 31 (3%) | 0/1010 (0.0%) | 37 | +| 폐 | 1050 | 1026 | 735/158/157 | 67.0 (60–73, n=968) | 40% | 513 (49%) | 280 (27%) | 166 (16%) | 34 (3%) | 57 (5%) | 0/1050 (0.0%) | 69 | +| 대장 | 534 | 523 | 374/80/80 | 67.0 (57–75, n=532) | 48% | 95 (18%) | 201 (38%) | 151 (28%) | 73 (14%) | 14 (3%) | 0/534 (0.0%) | 37 | +| 위 | 440 | 439 | 308/66/66 | 67.0 (58–73, n=436) | 35% | 58 (13%) | 131 (30%) | 188 (43%) | 44 (10%) | 19 (4%) | 440/440 (100.0%) | 22 | +| 두경부 | 523 | 468 | 366/79/78 | 61.0 (53–68, n=522) | 27% | 27 (5%) | 77 (15%) | 80 (15%) | 269 (51%) | 70 (13%) | 519/523 (99.2%) | 28 | + +## Table 1b — 엔드포인트별 유병률·결측 (사전등록 임계 n_pos≥25 대비) + +| 암종 | 엔드포인트 | assay source | 전체 코호트 라벨 결측 | 유병률(코호트 전체) | holdout n(denominator) | holdout n_pos | n_pos≥25? | +|---|---|---|---|---|---|---|---| +| 유방 | er_status | IHC(임상 병리검사) | 0.0% | 782/1010 (77%) | 1010 | 782 | n/a(전체 코호트 표기, holdout 분리 미실시) | +| 유방 | pr_status | IHC(임상 병리검사) | 0.5% | 672/1005 (67%) | 1010 | 672 | n/a(전체 코호트 표기, holdout 분리 미실시) | +| 유방 | her2_status | IHC/FISH(임상 병리검사, BIOP02-49 QC 완료) | 30.9% | 159/698 (23%) | 1010 | 159 | n/a(전체 코호트 표기, holdout 분리 미실시) | +| 유방 | pam50 | 유전자발현 분류기(계산값, tcga_brca_pam50_computed.csv) | 12.7% | LumB=321; LumA=243; Basal=198; HER2=120 | 1010 | — | n/a(다중클래스, 이진 유병률 미정의) | +| 폐 | egfr_activating | 체세포변이 콜(MAF, WES-derived) | 0.0% | 61/1050 (6%) | 271 | 15 | ⚠️ exploratory(<25) | +| 폐 | kras_g12c | 체세포변이 콜(MAF, WES-derived) | 0.0% | 70/1050 (7%) | 271 | 14 | ⚠️ exploratory(<25) | +| 폐 | histology_lusc | 병리 진단(조직형, 임상) | 0.0% | 484/1050 (46%) | 271 | 153 | ✅ | +| 대장 | braf_v600e | 체세포변이 콜(MAF, WES-derived) | 0.0% | 48/534 (9%) | 151 | 15 | ⚠️ exploratory(<25) | +| 위 | msi_h | MSIsensor score≥3.5(NGS-derived, SUBTYPE STAD_MSI 교차확인) | 0.5% | 82/438 (19%) | 107 | 24 | ⚠️ exploratory(<25) | +| 위 | erbb2_amp | CNA(GISTIC) 증폭 콜 | 0.5% | 58/438 (13%) | 107 | 14 | ⚠️ exploratory(<25) | +| 위 | lauren_diffuse | 병리 조직분류(ICD_O_3_HISTOLOGY, 임상) | 61.4% | 83/170 (49%) | 58 | 31 | ✅ | +| 위 | ebv | TCGA 분자아형(SUBTYPE, 바이러스 검출 복합판정) | 13.0% | 30/383 (8%) | 89 | 7 | ⚠️ exploratory(<25) | +| 두경부 | hpv_pos | TCGA 분자아형(SUBTYPE HNSC_HPV+, p16+바이러스검출 복합판정) | 6.9% | 72/487 (15%) | 135 | 26 | ✅ | +| 두경부 | egfr_amp | CNA(GISTIC) 증폭 콜 | 1.1% | 54/517 (10%) | 152 | 17 | ⚠️ exploratory(<25) | +| 두경부 | grade_high | 병리 등급(SAMPLE-level GRADE, 임상) | 4.2% | 130/501 (26%) | 147 | 41 | ✅ | diff --git a/experiments/crosscancer/table1/build_table1.py b/experiments/crosscancer/table1/build_table1.py new file mode 100644 index 0000000..b42faf6 --- /dev/null +++ b/experiments/crosscancer/table1/build_table1.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +BIOP02-148 — Table 1 조립. 신규 계산 없음 — 이미 커밋된 정본 파일에서 집계만 한다: + patient_labels.csv / split.csv / manifest (n환자, n슬라이드, split, 엔드포인트 n_pos/결측) + mil_cost_results.json (엔드포인트별 holdout n — denominator, 암종·엔드포인트마다 다름) + table1_clinical.json (fetch_table1_clinical.py 산출 — age/sex/stage/grade/TSS site) +출력: table1_master.json(정본, 모든 표 숫자의 단일 출처) + TABLE1.md + PARTICIPANT_FLOW.md +""" +import csv, json +from pathlib import Path +from collections import Counter + +HERE = Path(__file__).parent +CC = HERE.parent +DATA = CC.parent.parent / "agents" / "data" / "manifests" + +CLINICAL = json.load(open(HERE / "table1_clinical.json")) + +# 암종별 엔드포인트 assay-source(팀 기존 fetch 스크립트 주석/문서에서 확인, 신규 조사 아님) +ASSAY_SOURCE = { + "er_status": "IHC(임상 병리검사)", "pr_status": "IHC(임상 병리검사)", + "her2_status": "IHC/FISH(임상 병리검사, BIOP02-49 QC 완료)", + "pam50": "유전자발현 분류기(계산값, tcga_brca_pam50_computed.csv)", + "egfr_activating": "체세포변이 콜(MAF, WES-derived)", "kras_g12c": "체세포변이 콜(MAF, WES-derived)", + "histology_lusc": "병리 진단(조직형, 임상)", + "braf_v600e": "체세포변이 콜(MAF, WES-derived)", + "msi_h": "MSIsensor score≥3.5(NGS-derived, SUBTYPE STAD_MSI 교차확인)", + "erbb2_amp": "CNA(GISTIC) 증폭 콜", "lauren_diffuse": "병리 조직분류(ICD_O_3_HISTOLOGY, 임상)", + "ebv": "TCGA 분자아형(SUBTYPE, 바이러스 검출 복합판정)", + "hpv_pos": "TCGA 분자아형(SUBTYPE HNSC_HPV+, p16+바이러스검출 복합판정)", + "egfr_amp": "CNA(GISTIC) 증폭 콜", "grade_high": "병리 등급(SAMPLE-level GRADE, 임상)", +} + +master = {"cancers": {}} + +# ---------- BREAST ---------- +rows = list(csv.DictReader(open(DATA / "tcga_brca_manifest.csv"))) +split_ct = Counter(r["split"] for r in rows) +ep_rows = {} +for ep, has_col in [("er_status", "has_er"), ("pr_status", "has_pr"), + ("her2_status", "has_her2"), ("pam50", "has_pam50")]: + n_has = sum(1 for r in rows if r[has_col] == "1") + if ep == "pam50": + # 다중클래스(5종) — 이진 "양성" 개념 없음. has_pam50=1은 Normal-like 제외 로직 + # 이미 반영(her2_pam50_label_qc_v0.1.md §4, BIOP02-49 QC 확인) — 그대로 인용. + class_dist = dict(Counter(r[ep] for r in rows if r[has_col] == "1")) + n_pos = None + ep_rows[ep] = {"n_holdout_or_total": len(rows), "n_has_label": n_has, + "class_dist": class_dist, "n_pos": None, + "missing_pct": round(100 * (1 - n_has / len(rows)), 1), + "assay_source": ASSAY_SOURCE[ep]} + continue + n_pos = sum(1 for r in rows if r[has_col] == "1" and r[ep] == "Positive") + ep_rows[ep] = {"n_holdout_or_total": len(rows), "n_has_label": n_has, "n_pos": n_pos, + "missing_pct": round(100 * (1 - n_has / len(rows)), 1), + "assay_source": ASSAY_SOURCE[ep]} +master["cancers"]["BREAST"] = { + "n_patients": len(rows), "n_slides": len(rows), + "split": dict(split_ct), "n_holdout": split_ct["val"] + split_ct["test"], + **CLINICAL["BREAST"], "endpoints": ep_rows, +} + +# ---------- cross-cancer (LUNG/COLORECTAL/GASTRIC/HEADNECK) ---------- +CC_MAP = { + "LUNG": ("LUNG_NSCLC", ["egfr_activating", "kras_g12c", "histology_lusc"]), + "COLORECTAL": ("COLORECTAL", ["braf_v600e"]), + "GASTRIC": ("GASTRIC_STAD", ["msi_h", "erbb2_amp", "lauren_diffuse", "ebv"]), + "HEADNECK": ("HEADNECK_HNSC", ["hpv_pos", "egfr_amp", "grade_high"]), +} +for cancer, (dirname, endpoints) in CC_MAP.items(): + full = CC / dirname / "full" + labels = list(csv.DictReader(open(full / "patient_labels.csv"))) + split = list(csv.DictReader(open(full / "split.csv"))) + split_ct = Counter(r["split"] for r in split) + mil = json.load(open(full / "mil_cost_results.json")) + n_slides = mil["n_slides"] + + ep_rows = {} + for ep in endpoints: + has_col = "has_" + {"egfr_activating": "egfr", "kras_g12c": "kras_g12c", + "histology_lusc": "histology", "braf_v600e": "braf", + "msi_h": "msi", "erbb2_amp": "erbb2_amp", + "lauren_diffuse": "lauren", "ebv": "ebv", + "hpv_pos": "hpv", "egfr_amp": "egfr_amp", + "grade_high": "grade"}[ep] + n_has = sum(1 for r in labels if r.get(has_col) == "1") + n_pos = sum(1 for r in labels if r.get(has_col) == "1" and r.get(ep) == "1") + holdout_rec = mil["endpoints"].get(ep, {}).get("real", {}) + ep_rows[ep] = { + "n_total_cohort": len(labels), "n_has_label": n_has, "n_pos_cohort": n_pos, + "missing_pct": round(100 * (1 - n_has / len(labels)), 1), + "n_holdout": holdout_rec.get("n_holdout_patients"), "n_pos_holdout": holdout_rec.get("n_pos"), + "assay_source": ASSAY_SOURCE[ep], + } + master["cancers"][cancer] = { + "n_patients": len(labels), "n_slides": n_slides, + "split": dict(split_ct), "n_holdout": split_ct["val"] + split_ct["test"], + **CLINICAL[cancer], "endpoints": ep_rows, + } + +out = HERE / "table1_master.json" +json.dump(master, open(out, "w"), indent=2, ensure_ascii=False) +print(f"Saved {out}") + + +# ---------- TABLE1.md ---------- +def stagepct(d, key): + tot = sum(d.get(k, 0) for k in ("I", "II", "III", "IV", "missing")) + n = d.get(key, 0) + return f"{n} ({100*n/tot:.0f}%)" if tot else "—" + + +lines = ["# Table 1 — 코호트 baseline 특성 (5암종)", "", + "`claim_level: descriptive` — 사전등록 대상 없음(가설 검정 아님). " + "모든 수치는 `table1_master.json`(정본)에서 그대로 인용, 이 문서에서 재계산 없음.", "", + "| 암종 | N 환자 | N 슬라이드 | Train/Val/Test | 나이 중앙값(IQR) | 여성 % | " + "Stage I | II | III | IV | missing | Grade 커버리지 | TSS site 수 |", + "|---|---|---|---|---|---|---|---|---|---|---|---|---|"] +DISPLAY = {"BREAST": "유방", "LUNG": "폐", "COLORECTAL": "대장", "GASTRIC": "위", "HEADNECK": "두경부"} +for cancer in ["BREAST", "LUNG", "COLORECTAL", "GASTRIC", "HEADNECK"]: + c = master["cancers"][cancer] + age = c["age"] + age_s = f"{age['median']} ({age['iqr'][0]:.0f}–{age['iqr'][1]:.0f}, n={age['n']})" if age else "—" + sex = c["sex_dist"] + tot_sex = sum(sex.values()) + female_pct = f"{100*sex.get('Female',0)/tot_sex:.0f}%" if tot_sex else "—" + sd = c["stage_dist"] + gc = f"{c['grade_coverage_n']}/{c['n_patients']} ({c['grade_coverage_pct']}%)" + sp = c["split"] + lines.append( + f"| {DISPLAY[cancer]} | {c['n_patients']} | {c['n_slides']} | " + f"{sp.get('train',0)}/{sp.get('val',0)}/{sp.get('test',0)} | {age_s} | {female_pct} | " + f"{stagepct(sd,'I')} | {stagepct(sd,'II')} | {stagepct(sd,'III')} | {stagepct(sd,'IV')} | " + f"{stagepct(sd,'missing')} | {gc} | {c['tss_n_sites']} |" + ) + +lines += ["", "## Table 1b — 엔드포인트별 유병률·결측 (사전등록 임계 n_pos≥25 대비)", "", + "| 암종 | 엔드포인트 | assay source | 전체 코호트 라벨 결측 | 유병률(코호트 전체) | " + "holdout n(denominator) | holdout n_pos | n_pos≥25? |", + "|---|---|---|---|---|---|---|---|"] +for cancer in ["BREAST", "LUNG", "COLORECTAL", "GASTRIC", "HEADNECK"]: + c = master["cancers"][cancer] + for ep, e in c["endpoints"].items(): + if cancer == "BREAST": + n_pos_h = e["n_pos"] + if ep == "pam50": + cd = e["class_dist"] + prev = "; ".join(f"{k}={v}" for k, v in sorted(cd.items(), key=lambda x: -x[1])) + gate = "n/a(다중클래스, 이진 유병률 미정의)" + else: + prev = f"{e['n_pos']}/{e['n_has_label']} ({100*e['n_pos']/e['n_has_label']:.0f}%)" if e["n_has_label"] else "—" + gate = "n/a(전체 코호트 표기, holdout 분리 미실시)" + else: + n_pos_h = e["n_pos_holdout"] + prev = f"{e['n_pos_cohort']}/{e['n_has_label']} ({100*e['n_pos_cohort']/e['n_has_label']:.0f}%)" if e["n_has_label"] else "—" + gate = ("✅" if (n_pos_h or 0) >= 25 else "⚠️ exploratory(<25)") if n_pos_h is not None else "—" + lines.append(f"| {DISPLAY[cancer]} | {ep} | {e['assay_source']} | {e['missing_pct']}% | " + f"{prev} | {e.get('n_holdout', e.get('n_holdout_or_total','—'))} | " + f"{n_pos_h if n_pos_h is not None else '—'} | {gate} |") + +(HERE / "TABLE1.md").write_text("\n".join(lines) + "\n") +print(f"Saved {HERE/'TABLE1.md'}") + + +# ---------- PARTICIPANT_FLOW.md ---------- +pf = ["# Participant flow — 5암종 (TRIPOD+AI 13b 짝)", "", + "`n식별` = patient_labels.csv/manifest에 라벨이 조인된 환자 수(=GDC 매니페스트에서 " + "H&E 슬라이드+임상데이터 결합 가능 subset, 이미 필터링된 상태 — raw GDC 전체 대비 " + "제외 사유는 각 코호트 원 매니페스트 문서 참조). 여기서부터 아래로: 라벨 결측 없는 " + "환자 → train/holdout 분리.", "", + "| 암종 | n(라벨 조인, 시작점) | n(슬라이드 보유) | n(train) | n(holdout=val+test) |", + "|---|---|---|---|---|"] +for cancer in ["BREAST", "LUNG", "COLORECTAL", "GASTRIC", "HEADNECK"]: + c = master["cancers"][cancer] + sp = c["split"] + pf.append(f"| {DISPLAY[cancer]} | {c['n_patients']} | {c['n_slides']} | " + f"{sp.get('train',0)} | {c['n_holdout']} |") +pf += ["", "엔드포인트별 최종 분석 대상(holdout denominator)은 Table 1b 참조 — " + "암종 안에서도 엔드포인트마다 라벨 결측 패턴이 달라 holdout n이 다르다(예: 위 " + "lauren_diffuse n=58 vs msi_h n=107, 같은 GASTRIC holdout 132명 중 라벨 있는 하위집합만)."] +(HERE / "PARTICIPANT_FLOW.md").write_text("\n".join(pf) + "\n") +print(f"Saved {HERE/'PARTICIPANT_FLOW.md'}") +print("DONE_BUILD_TABLE1") diff --git a/experiments/crosscancer/table1/check_table1_numbers.py b/experiments/crosscancer/table1/check_table1_numbers.py new file mode 100644 index 0000000..11a3ba1 --- /dev/null +++ b/experiments/crosscancer/table1/check_table1_numbers.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +BIOP02-148 DoD "모든 수치가 커밋된 결과 파일과 1:1 대조" + "본문 R1/R2와 분모 불일치 0건" 검증. +check_number_drift.py(BIOP02-107)는 AUROC류만, 3개 고정 문서만 본다 — Table 1의 환자수/ +n_pos/결측 수치는 범위 밖이라 이 스크립트로 별도 확인한다. 판정 아님(원 스크립트와 동일 +철학) — 불일치 후보만 나열, 사람이 확인. +""" +import csv, json, re, sys +from pathlib import Path + +HERE = Path(__file__).parent +REPO = HERE.parent.parent.parent +MASTER = json.load(open(HERE / "table1_master.json")) + +problems = [] + +# 1) table1_master.json의 holdout n_pos가 원천 mil_cost_results.json과 정확히 같은가 +CC_MAP = {"LUNG": "LUNG_NSCLC", "COLORECTAL": "COLORECTAL", "GASTRIC": "GASTRIC_STAD", + "HEADNECK": "HEADNECK_HNSC"} +for cancer, dirname in CC_MAP.items(): + mil = json.load(open(HERE.parent / dirname / "full" / "mil_cost_results.json")) + for ep, e in MASTER["cancers"][cancer]["endpoints"].items(): + src = mil["endpoints"][ep]["real"] + if e["n_pos_holdout"] != src.get("n_pos") or e["n_holdout"] != src.get("n_holdout_patients"): + problems.append(f"[master vs mil_cost_results.json] {cancer}.{ep}: " + f"master n_pos={e['n_pos_holdout']} n_holdout={e['n_holdout']} vs " + f"source n_pos={src.get('n_pos')} n_holdout={src.get('n_holdout_patients')}") + +# 2) manuscript R2 표(축 | 홀드아웃 양성 표본 | 판정)와 대조 +R2_ROW = re.compile(r"^\|\s*(.+?)\s*\|\s*(\d+)\s*\|\s*(.+?)\s*\|$") +R2_LABEL_TO_MASTER = { + "폐 EGFR 활성변이": ("LUNG", "egfr_activating"), + "폐 KRAS-G12C": ("LUNG", "kras_g12c"), + "위 ERBB2 증폭": ("GASTRIC", "erbb2_amp"), + "위 MSI-H": ("GASTRIC", "msi_h"), + "위 EBV": ("GASTRIC", "ebv"), + "두경부 EGFR 증폭": ("HEADNECK", "egfr_amp"), +} +results_md = REPO / "manuscript" / "sections" / "02_results.md" +r2_checked = 0 +if results_md.exists(): + for line in open(results_md, encoding="utf-8"): + m = R2_ROW.match(line.strip()) + if not m: + continue + label, n_pos_str, verdict = m.groups() + if label not in R2_LABEL_TO_MASTER: + continue + cancer, ep = R2_LABEL_TO_MASTER[label] + master_n = MASTER["cancers"][cancer]["endpoints"][ep]["n_pos_holdout"] + r2_checked += 1 + if master_n != int(n_pos_str): + problems.append(f"[Table1 vs 02_results.md R2] {label}: 본문={n_pos_str}, " + f"table1_master={master_n}") +print(f"R2 표 {r2_checked}/6행 대조 완료") + +# 3) R1 prose에 인용된 헤드라인 n_pos(폐 LUSC 153, 두경부 HPV 26, 두경부 grade 41) +R1_CITED = {"폐 조직형 LUSC 153명": ("LUNG", "histology_lusc", 153), + "두경부 HPV 26명": ("HEADNECK", "hpv_pos", 26), + "두경부 grade 41명": ("HEADNECK", "grade_high", 41)} +for label, (cancer, ep, cited_n) in R1_CITED.items(): + master_n = MASTER["cancers"][cancer]["endpoints"][ep]["n_pos_holdout"] + if master_n != cited_n: + problems.append(f"[Table1 vs 02_results.md R1 prose] {label}: 본문 인용={cited_n}, " + f"table1_master={master_n}") + +print(f"\n[결과] 불일치 후보 {len(problems)}건") +for p in problems: + print(" ", p) +if not problems: + print("모든 대조 통과 — Table 1/PARTICIPANT_FLOW의 n_pos·holdout 수치가 " + "mil_cost_results.json(정본) 및 02_results.md R1/R2 인용과 1:1 일치.") +sys.exit(1 if problems else 0) diff --git a/experiments/crosscancer/table1/fetch_table1_clinical.py b/experiments/crosscancer/table1/fetch_table1_clinical.py new file mode 100644 index 0000000..2635da9 --- /dev/null +++ b/experiments/crosscancer/table1/fetch_table1_clinical.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +BIOP02-148 — Table 1(코호트 특성표) 공용 임상변수(age/sex/stage/grade/TSS site) fetch. +5암종 전부 cBioPortal PATIENT-level clinical-data, 각 코호트 기존 case_id universe에 join만 +(신규 코호트 정의 없음 — patient_labels.csv/manifest 그대로 재사용). +""" +import csv, json, sys, urllib.request +from pathlib import Path +from collections import Counter + +HERE = Path(__file__).parent +CC = HERE.parent +DATA = CC.parent.parent / "agents" / "data" / "manifests" + +STUDIES = { + "BREAST": ["brca_tcga_pan_can_atlas_2018"], + "LUNG": ["luad_tcga_pan_can_atlas_2018", "lusc_tcga_pan_can_atlas_2018"], + "COLORECTAL": ["coadread_tcga_pan_can_atlas_2018"], + "GASTRIC": ["stad_tcga_pan_can_atlas_2018"], + "HEADNECK": ["hnsc_tcga_pan_can_atlas_2018"], +} +ATTRS = ["AGE", "SEX", "AJCC_PATHOLOGIC_TUMOR_STAGE", "GRADE", "TISSUE_SOURCE_SITE_CODE"] + + +def post(path, body): + for a in range(4): + try: + 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=60)) + except Exception as e: + print(f" cbio POST {path} retry{a+1}: {e}"); import time; time.sleep(6 * (a + 1)) + raise RuntimeError(f"cbio POST failed {path}") + + +def stage_ordinal(v): + if not v or v.startswith("["): + return "missing" + v = v.upper().replace("STAGE ", "").strip() + for pfx in ("IV", "III", "II", "I"): + if v.startswith(pfx): + return pfx + return "missing" + + +def case_universe(cancer): + if cancer == "BREAST": + rows = list(csv.DictReader(open(DATA / "tcga_brca_manifest.csv"))) + return sorted({r["case_id"] for r in rows}) + d = {"LUNG": "LUNG_NSCLC", "COLORECTAL": "COLORECTAL", "GASTRIC": "GASTRIC_STAD", + "HEADNECK": "HEADNECK_HNSC"}[cancer] + rows = list(csv.DictReader(open(CC / d / "full" / "patient_labels.csv"))) + return sorted({r["case_id"] for r in rows}) + + +def main(): + out_all = {} + for cancer, studies in STUDIES.items(): + cases = set(case_universe(cancer)) + print(f"[{cancer}] target patients: {len(cases)}") + age, sex, stage_raw, grade_raw, tss = {}, {}, {}, {}, {} + for study in studies: + d = post(f"/studies/{study}/clinical-data/fetch?clinicalDataType=PATIENT", + {"attributeIds": ["AGE", "SEX", "AJCC_PATHOLOGIC_TUMOR_STAGE"]}) + for x in d: + pid = x["patientId"] + if pid not in cases: + continue + aid, val = x["clinicalAttributeId"], x["value"] + if aid == "AGE": + try: age[pid] = float(val) + except (TypeError, ValueError): pass + elif aid == "SEX": + sex[pid] = val + elif aid == "AJCC_PATHOLOGIC_TUMOR_STAGE": + stage_raw[pid] = val + # GRADE/TISSUE_SOURCE_SITE_CODE는 SAMPLE-level(sh_fetch_labels.py 확인 패턴과 동일) — + # PATIENT-level로 조회하면 0건이 나오는 걸 실측으로 확인(2026-08-20). + ds = post(f"/studies/{study}/clinical-data/fetch?clinicalDataType=SAMPLE", + {"attributeIds": ["GRADE", "TISSUE_SOURCE_SITE_CODE"]}) + for x in ds: + pid = x["patientId"] + if pid not in cases: + continue + aid, val = x["clinicalAttributeId"], x["value"] + if aid == "GRADE" and pid not in grade_raw: # 환자당 첫 샘플만(중복 방지) + grade_raw[pid] = val + elif aid == "TISSUE_SOURCE_SITE_CODE" and pid not in tss: + tss[pid] = val + import statistics as st + ages = sorted(age.values()) + age_summary = None + if ages: + q1 = ages[int(0.25 * (len(ages) - 1))]; q3 = ages[int(0.75 * (len(ages) - 1))] + age_summary = {"median": round(st.median(ages), 1), "iqr": [q1, q3], "n": len(ages)} + sex_dist = Counter(sex.values()) + stage_dist = Counter(stage_ordinal(stage_raw.get(c, "")) for c in cases) + grade_n = sum(1 for c in cases if grade_raw.get(c)) + grade_dist = Counter(grade_raw.get(c, "missing") for c in cases if grade_raw.get(c)) + tss_n_sites = len({tss[c] for c in cases if c in tss}) + + out_all[cancer] = { + "n_patients": len(cases), + "age": age_summary, + "sex_dist": dict(sex_dist), + "stage_dist": dict(stage_dist), + "grade_coverage_n": grade_n, "grade_coverage_pct": round(100 * grade_n / len(cases), 1), + "grade_dist": dict(grade_dist), + "tss_n_sites": tss_n_sites, + } + print(f" age n={age_summary['n'] if age_summary else 0}, sex={dict(sex_dist)}, " + f"stage={dict(stage_dist)}, grade_coverage={grade_n}/{len(cases)}, tss_sites={tss_n_sites}") + + out = HERE / "table1_clinical.json" + json.dump(out_all, open(out, "w"), indent=2, ensure_ascii=False) + print(f"Saved {out}\nDONE_FETCH_TABLE1_CLINICAL") + + +if __name__ == "__main__": + main() diff --git a/experiments/crosscancer/table1/table1_clinical.json b/experiments/crosscancer/table1/table1_clinical.json new file mode 100644 index 0000000..ca3a107 --- /dev/null +++ b/experiments/crosscancer/table1/table1_clinical.json @@ -0,0 +1,143 @@ +{ + "BREAST": { + "n_patients": 1010, + "age": { + "median": 58.5, + "iqr": [ + 49.0, + 68.0 + ], + "n": 996 + }, + "sex_dist": { + "Female": 984, + "Male": 12 + }, + "stage_dist": { + "II": 569, + "I": 167, + "III": 227, + "missing": 31, + "IV": 16 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 37 + }, + "LUNG": { + "n_patients": 1050, + "age": { + "median": 67.0, + "iqr": [ + 60.0, + 73.0 + ], + "n": 968 + }, + "sex_dist": { + "Male": 594, + "Female": 402 + }, + "stage_dist": { + "III": 166, + "II": 280, + "I": 513, + "missing": 57, + "IV": 34 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 69 + }, + "COLORECTAL": { + "n_patients": 534, + "age": { + "median": 67.0, + "iqr": [ + 57.0, + 75.0 + ], + "n": 532 + }, + "sex_dist": { + "Male": 277, + "Female": 255 + }, + "stage_dist": { + "III": 151, + "II": 201, + "IV": 73, + "I": 95, + "missing": 14 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 37 + }, + "GASTRIC": { + "n_patients": 440, + "age": { + "median": 67.0, + "iqr": [ + 58.0, + 73.0 + ], + "n": 436 + }, + "sex_dist": { + "Male": 284, + "Female": 156 + }, + "stage_dist": { + "missing": 19, + "II": 131, + "I": 58, + "III": 188, + "IV": 44 + }, + "grade_coverage_n": 440, + "grade_coverage_pct": 100.0, + "grade_dist": { + "G3": 263, + "G2": 156, + "GX": 9, + "G1": 12 + }, + "tss_n_sites": 22 + }, + "HEADNECK": { + "n_patients": 523, + "age": { + "median": 61.0, + "iqr": [ + 53.0, + 68.0 + ], + "n": 522 + }, + "sex_dist": { + "Male": 382, + "Female": 141 + }, + "stage_dist": { + "IV": 269, + "III": 80, + "missing": 70, + "I": 27, + "II": 77 + }, + "grade_coverage_n": 519, + "grade_coverage_pct": 99.2, + "grade_dist": { + "G3": 123, + "G2": 308, + "G1": 63, + "GX": 18, + "G4": 7 + }, + "tss_n_sites": 28 + } +} \ No newline at end of file diff --git a/experiments/crosscancer/table1/table1_master.json b/experiments/crosscancer/table1/table1_master.json new file mode 100644 index 0000000..5e90414 --- /dev/null +++ b/experiments/crosscancer/table1/table1_master.json @@ -0,0 +1,323 @@ +{ + "cancers": { + "BREAST": { + "n_patients": 1010, + "n_slides": 1010, + "split": { + "test": 151, + "val": 152, + "train": 707 + }, + "n_holdout": 303, + "age": { + "median": 58.5, + "iqr": [ + 49.0, + 68.0 + ], + "n": 996 + }, + "sex_dist": { + "Female": 984, + "Male": 12 + }, + "stage_dist": { + "II": 569, + "I": 167, + "III": 227, + "missing": 31, + "IV": 16 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 37, + "endpoints": { + "er_status": { + "n_holdout_or_total": 1010, + "n_has_label": 1010, + "n_pos": 782, + "missing_pct": 0.0, + "assay_source": "IHC(임상 병리검사)" + }, + "pr_status": { + "n_holdout_or_total": 1010, + "n_has_label": 1005, + "n_pos": 672, + "missing_pct": 0.5, + "assay_source": "IHC(임상 병리검사)" + }, + "her2_status": { + "n_holdout_or_total": 1010, + "n_has_label": 698, + "n_pos": 159, + "missing_pct": 30.9, + "assay_source": "IHC/FISH(임상 병리검사, BIOP02-49 QC 완료)" + }, + "pam50": { + "n_holdout_or_total": 1010, + "n_has_label": 882, + "class_dist": { + "HER2": 120, + "LumB": 321, + "LumA": 243, + "Basal": 198 + }, + "n_pos": null, + "missing_pct": 12.7, + "assay_source": "유전자발현 분류기(계산값, tcga_brca_pam50_computed.csv)" + } + } + }, + "LUNG": { + "n_patients": 1050, + "n_slides": 1026, + "split": { + "train": 735, + "val": 158, + "test": 157 + }, + "n_holdout": 315, + "age": { + "median": 67.0, + "iqr": [ + 60.0, + 73.0 + ], + "n": 968 + }, + "sex_dist": { + "Male": 594, + "Female": 402 + }, + "stage_dist": { + "III": 166, + "II": 280, + "I": 513, + "missing": 57, + "IV": 34 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 69, + "endpoints": { + "egfr_activating": { + "n_total_cohort": 1050, + "n_has_label": 1050, + "n_pos_cohort": 61, + "missing_pct": 0.0, + "n_holdout": 271, + "n_pos_holdout": 15, + "assay_source": "체세포변이 콜(MAF, WES-derived)" + }, + "kras_g12c": { + "n_total_cohort": 1050, + "n_has_label": 1050, + "n_pos_cohort": 70, + "missing_pct": 0.0, + "n_holdout": 271, + "n_pos_holdout": 14, + "assay_source": "체세포변이 콜(MAF, WES-derived)" + }, + "histology_lusc": { + "n_total_cohort": 1050, + "n_has_label": 1050, + "n_pos_cohort": 484, + "missing_pct": 0.0, + "n_holdout": 271, + "n_pos_holdout": 153, + "assay_source": "병리 진단(조직형, 임상)" + } + } + }, + "COLORECTAL": { + "n_patients": 534, + "n_slides": 523, + "split": { + "test": 80, + "val": 80, + "train": 374 + }, + "n_holdout": 160, + "age": { + "median": 67.0, + "iqr": [ + 57.0, + 75.0 + ], + "n": 532 + }, + "sex_dist": { + "Male": 277, + "Female": 255 + }, + "stage_dist": { + "III": 151, + "II": 201, + "IV": 73, + "I": 95, + "missing": 14 + }, + "grade_coverage_n": 0, + "grade_coverage_pct": 0.0, + "grade_dist": {}, + "tss_n_sites": 37, + "endpoints": { + "braf_v600e": { + "n_total_cohort": 534, + "n_has_label": 534, + "n_pos_cohort": 48, + "missing_pct": 0.0, + "n_holdout": 151, + "n_pos_holdout": 15, + "assay_source": "체세포변이 콜(MAF, WES-derived)" + } + } + }, + "GASTRIC": { + "n_patients": 440, + "n_slides": 439, + "split": { + "val": 66, + "test": 66, + "train": 308 + }, + "n_holdout": 132, + "age": { + "median": 67.0, + "iqr": [ + 58.0, + 73.0 + ], + "n": 436 + }, + "sex_dist": { + "Male": 284, + "Female": 156 + }, + "stage_dist": { + "missing": 19, + "II": 131, + "I": 58, + "III": 188, + "IV": 44 + }, + "grade_coverage_n": 440, + "grade_coverage_pct": 100.0, + "grade_dist": { + "G3": 263, + "G2": 156, + "GX": 9, + "G1": 12 + }, + "tss_n_sites": 22, + "endpoints": { + "msi_h": { + "n_total_cohort": 440, + "n_has_label": 438, + "n_pos_cohort": 82, + "missing_pct": 0.5, + "n_holdout": 107, + "n_pos_holdout": 24, + "assay_source": "MSIsensor score≥3.5(NGS-derived, SUBTYPE STAD_MSI 교차확인)" + }, + "erbb2_amp": { + "n_total_cohort": 440, + "n_has_label": 438, + "n_pos_cohort": 58, + "missing_pct": 0.5, + "n_holdout": 107, + "n_pos_holdout": 14, + "assay_source": "CNA(GISTIC) 증폭 콜" + }, + "lauren_diffuse": { + "n_total_cohort": 440, + "n_has_label": 170, + "n_pos_cohort": 83, + "missing_pct": 61.4, + "n_holdout": 58, + "n_pos_holdout": 31, + "assay_source": "병리 조직분류(ICD_O_3_HISTOLOGY, 임상)" + }, + "ebv": { + "n_total_cohort": 440, + "n_has_label": 383, + "n_pos_cohort": 30, + "missing_pct": 13.0, + "n_holdout": 89, + "n_pos_holdout": 7, + "assay_source": "TCGA 분자아형(SUBTYPE, 바이러스 검출 복합판정)" + } + } + }, + "HEADNECK": { + "n_patients": 523, + "n_slides": 468, + "split": { + "train": 366, + "val": 79, + "test": 78 + }, + "n_holdout": 157, + "age": { + "median": 61.0, + "iqr": [ + 53.0, + 68.0 + ], + "n": 522 + }, + "sex_dist": { + "Male": 382, + "Female": 141 + }, + "stage_dist": { + "IV": 269, + "III": 80, + "missing": 70, + "I": 27, + "II": 77 + }, + "grade_coverage_n": 519, + "grade_coverage_pct": 99.2, + "grade_dist": { + "G3": 123, + "G2": 308, + "G1": 63, + "GX": 18, + "G4": 7 + }, + "tss_n_sites": 28, + "endpoints": { + "hpv_pos": { + "n_total_cohort": 523, + "n_has_label": 487, + "n_pos_cohort": 72, + "missing_pct": 6.9, + "n_holdout": 135, + "n_pos_holdout": 26, + "assay_source": "TCGA 분자아형(SUBTYPE HNSC_HPV+, p16+바이러스검출 복합판정)" + }, + "egfr_amp": { + "n_total_cohort": 523, + "n_has_label": 517, + "n_pos_cohort": 54, + "missing_pct": 1.1, + "n_holdout": 152, + "n_pos_holdout": 17, + "assay_source": "CNA(GISTIC) 증폭 콜" + }, + "grade_high": { + "n_total_cohort": 523, + "n_has_label": 501, + "n_pos_cohort": 130, + "missing_pct": 4.2, + "n_holdout": 147, + "n_pos_holdout": 41, + "assay_source": "병리 등급(SAMPLE-level GRADE, 임상)" + } + } + } + } +} \ No newline at end of file