Skip to content

Commit 668879f

Browse files
fedorovclaude
andcommitted
ENH: replace heuristic GDC project mapping with explicit dict and add validation
Replace the `collection_id.replace("_", "-").upper()` heuristic in `idc_collection_to_gdc_projects()` with an explicit `IDC_TO_GDC_PROJECTS` dict covering all mapped collections. This catches naming exceptions like `CDDP_EAGLE-1` (underscore in GDC project name) that the heuristic silently mishandled, producing zero GDC matches. Also adds `validate_gdc_matches()` which raises an error after the API run if any collection (outside known GDC-absent ones) returns zero matches, so future mapping errors are caught immediately rather than silently producing nulls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b1a098f commit 668879f

2 files changed

Lines changed: 124 additions & 27 deletions

File tree

scripts/gdc/IDC_GDC_MAPPING.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,23 @@ Checks which IDC patients from GDC-related collections exist as cases in GDC.
1919
### IDC collection to GDC project mapping
2020

2121
PatientIDs are only unique within a project/collection, so the GDC lookup is
22-
scoped by project:
23-
24-
- **TCGA**: direct 1:1 mapping (e.g. `tcga_brca` -> `TCGA-BRCA`)
25-
- **CCDI**: direct mapping (`ccdi_mci` -> `CCDI-MCI`)
26-
- **VAREPOP-APOLLO**: direct mapping (`varepop_apollo` -> `VAREPOP-APOLLO`)
27-
- **CPTAC**: IDC has per-cancer collections (`cptac_brca`, `cptac_luad`, ...)
28-
but GDC groups all CPTAC cases under umbrella projects `CPTAC-2` and
22+
scoped by project. The mapping is defined explicitly in `IDC_TO_GDC_PROJECTS` in
23+
`gdc_parquet_generator.py`. Notable cases:
24+
25+
- **TCGA**: 1:1 mapping (e.g. `tcga_brca``TCGA-BRCA`)
26+
- **CCDI**: `ccdi_mci``CCDI-MCI`
27+
- **VAREPOP-APOLLO**: `varepop_apollo``VAREPOP-APOLLO`
28+
- **CPTAC**: IDC has per-cancer sub-collections (`cptac_brca`, `cptac_luad`,
29+
...) but GDC groups all CPTAC cases under umbrella projects `CPTAC-2` and
2930
`CPTAC-3`, so all CPTAC patients are queried against both.
31+
- **CGCI**: `cgci_blgsp``CGCI-BLGSP`, `cgci_htmcp_cc``CGCI-HTMCP-CC`, etc.
32+
- **HCMI**: `hcmi_cmdc``HCMI-CMDC`
33+
- **CDDP-EAGLE-1**: `cddp_eagle_1``CDDP_EAGLE-1` — GDC uses an underscore
34+
between `CDDP` and `EAGLE` in the project name (not a hyphen).
35+
36+
When adding a new GDC collection to `idc_gdc_selection.sql`, also add its entry
37+
to `IDC_TO_GDC_PROJECTS`. The function raises `ValueError` for any collection
38+
not in the map, so missing entries are caught immediately.
3039

3140
### Output schema
3241

@@ -95,12 +104,11 @@ These patients exist in GDC but are not matched due to the ID discrepancy.
95104

96105
## Collection coverage analysis
97106

98-
GDC has 91 projects. We cross-referenced all IDC collection names against all
99-
GDC project IDs and confirmed that the current SQL filters (`tcga%`, `%apollo%`,
100-
`%cptac%`, `%ccdi%`) cover every IDC collection that has a corresponding GDC
101-
project.
107+
The SQL in `idc_gdc_selection.sql` selects all IDC collections whose
108+
`collection_id` matches one of the GDC-related LIKE patterns. When a new GDC
109+
collection is added, both the SQL pattern and `IDC_TO_GDC_PROJECTS` must be
110+
updated in tandem.
102111

103-
The remaining ~120 IDC collections (e.g. `acrin_*`, `cmb_*`, `htan_*`, `nlst`,
104-
`rider_*`, `qin_*`, etc.) do not correspond to any GDC project. Other GDC
105-
programs (TARGET, MATCH, CGCI, CMI, MMRF, HCMI, BEATAML, FM-AD, WCDT, etc.) do
106-
not have imaging collections in IDC.
112+
The remaining IDC collections (e.g. `acrin_*`, `cmb_*`, `htan_*`, `nlst`,
113+
`rider_*`, `qin_*`, etc.) do not correspond to any GDC project and are not
114+
included in this mapping.

scripts/gdc/gdc_parquet_generator.py

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Check which IDC PatientIDs from GDC-related collections exist in the Genomic Data Commons.
22
33
PatientID is only unique within a collection/project, so the GDC lookup is
4-
scoped by project. IDC collection_id maps to GDC project.project_id with one
5-
special case: all CPTAC sub-collections in IDC (cptac_brca, cptac_luad, ...)
6-
fall under CPTAC-2 or CPTAC-3 in GDC, so those are queried together.
4+
scoped by project. The mapping from IDC collection_id to GDC project_id is
5+
explicit in IDC_TO_GDC_PROJECTS; add new collections there when they are added
6+
to the SQL query.
77
"""
88

99
from __future__ import annotations
@@ -29,8 +29,73 @@
2929
SQL_FILE = Path(__file__).parent / "idc_gdc_selection.sql"
3030
OUTPUT_PARQUET = "gdc_idc_mapping.parquet"
3131

32-
# IDC CPTAC sub-collections map to these GDC umbrella projects.
33-
GDC_CPTAC_PROJECTS = ["CPTAC-2", "CPTAC-3"]
32+
# Explicit mapping from IDC collection_id to GDC project_id(s).
33+
# CPTAC sub-collections in IDC all map to the GDC umbrella projects CPTAC-2 and CPTAC-3.
34+
# CDDP_EAGLE-1 uses an underscore in the GDC project name (not a hyphen like most programs).
35+
IDC_TO_GDC_PROJECTS: dict[str, list[str]] = {
36+
# TCGA
37+
"tcga_acc": ["TCGA-ACC"],
38+
"tcga_blca": ["TCGA-BLCA"],
39+
"tcga_brca": ["TCGA-BRCA"],
40+
"tcga_cesc": ["TCGA-CESC"],
41+
"tcga_chol": ["TCGA-CHOL"],
42+
"tcga_coad": ["TCGA-COAD"],
43+
"tcga_dlbc": ["TCGA-DLBC"],
44+
"tcga_esca": ["TCGA-ESCA"],
45+
"tcga_gbm": ["TCGA-GBM"],
46+
"tcga_hnsc": ["TCGA-HNSC"],
47+
"tcga_kich": ["TCGA-KICH"],
48+
"tcga_kirc": ["TCGA-KIRC"],
49+
"tcga_kirp": ["TCGA-KIRP"],
50+
"tcga_lgg": ["TCGA-LGG"],
51+
"tcga_lihc": ["TCGA-LIHC"],
52+
"tcga_luad": ["TCGA-LUAD"],
53+
"tcga_lusc": ["TCGA-LUSC"],
54+
"tcga_meso": ["TCGA-MESO"],
55+
"tcga_ov": ["TCGA-OV"],
56+
"tcga_paad": ["TCGA-PAAD"],
57+
"tcga_pcpg": ["TCGA-PCPG"],
58+
"tcga_prad": ["TCGA-PRAD"],
59+
"tcga_read": ["TCGA-READ"],
60+
"tcga_sarc": ["TCGA-SARC"],
61+
"tcga_skcm": ["TCGA-SKCM"],
62+
"tcga_stad": ["TCGA-STAD"],
63+
"tcga_tgct": ["TCGA-TGCT"],
64+
"tcga_thca": ["TCGA-THCA"],
65+
"tcga_thym": ["TCGA-THYM"],
66+
"tcga_ucec": ["TCGA-UCEC"],
67+
"tcga_ucs": ["TCGA-UCS"],
68+
"tcga_uvm": ["TCGA-UVM"],
69+
# CPTAC (IDC per-cancer sub-collections map to GDC umbrella projects)
70+
"cptac_aml": ["CPTAC-2", "CPTAC-3"],
71+
"cptac_brca": ["CPTAC-2", "CPTAC-3"],
72+
"cptac_ccrcc": ["CPTAC-2", "CPTAC-3"],
73+
"cptac_cm": ["CPTAC-2", "CPTAC-3"],
74+
"cptac_coad": ["CPTAC-2", "CPTAC-3"],
75+
"cptac_gbm": ["CPTAC-2", "CPTAC-3"],
76+
"cptac_hnscc": ["CPTAC-2", "CPTAC-3"],
77+
"cptac_lscc": ["CPTAC-2", "CPTAC-3"],
78+
"cptac_luad": ["CPTAC-2", "CPTAC-3"],
79+
"cptac_ov": ["CPTAC-2", "CPTAC-3"],
80+
"cptac_pda": ["CPTAC-2", "CPTAC-3"],
81+
"cptac_sar": ["CPTAC-2", "CPTAC-3"],
82+
"cptac_stad": ["CPTAC-2", "CPTAC-3"],
83+
"cptac_ucec": ["CPTAC-2", "CPTAC-3"],
84+
# Other GDC programs
85+
"ccdi_mci": ["CCDI-MCI"],
86+
"varepop_apollo": ["VAREPOP-APOLLO"],
87+
# v24 new GDC collections
88+
"cddp_eagle_1": ["CDDP_EAGLE-1"],
89+
"cgci_blgsp": ["CGCI-BLGSP"],
90+
"cgci_htmcp_cc": ["CGCI-HTMCP-CC"],
91+
"cgci_htmcp_dlbcl": ["CGCI-HTMCP-DLBCL"],
92+
"cgci_htmcp_lc": ["CGCI-HTMCP-LC"],
93+
"hcmi_cmdc": ["HCMI-CMDC"],
94+
}
95+
96+
# Collections confirmed to have no cases in GDC (imaging-only, never registered).
97+
# These are excluded from the zero-match check in validate_gdc_matches().
98+
GDC_ABSENT_COLLECTIONS: frozenset[str] = frozenset({"cptac_cm", "cptac_sar"})
3499

35100

36101
def _create_session() -> requests.Session:
@@ -49,15 +114,16 @@ def _create_session() -> requests.Session:
49114

50115

51116
def idc_collection_to_gdc_projects(collection_id: str) -> list[str]:
52-
"""Map an IDC collection_id to the corresponding GDC project_id(s).
117+
"""Return the GDC project_id(s) for the given IDC collection_id.
53118
54-
For most collections the mapping is simply upper-case + hyphens
55-
(e.g. tcga_brca -> TCGA-BRCA). CPTAC is the exception: IDC has
56-
per-cancer collections but GDC groups them under CPTAC-2 / CPTAC-3.
119+
Raises ValueError for unknown collections so missing mappings are caught
120+
immediately rather than silently querying the wrong GDC project.
57121
"""
58-
if collection_id.startswith("cptac_"):
59-
return GDC_CPTAC_PROJECTS
60-
return [collection_id.replace("_", "-").upper()]
122+
projects = IDC_TO_GDC_PROJECTS.get(collection_id)
123+
if projects is None:
124+
msg = f"No GDC project mapping for IDC collection {collection_id!r} — add it to IDC_TO_GDC_PROJECTS"
125+
raise ValueError(msg)
126+
return projects
61127

62128

63129
def run_bigquery(project_id: str | None = None) -> pd.DataFrame:
@@ -266,6 +332,28 @@ def save_results(df: pd.DataFrame, output_path: str = OUTPUT_PARQUET) -> None:
266332
con.close()
267333

268334

335+
def validate_gdc_matches(studies_df: pd.DataFrame) -> None:
336+
"""Raise an error if any mapped collection has zero GDC case ID matches.
337+
338+
Collections in GDC_ABSENT_COLLECTIONS are exempt (confirmed not in GDC).
339+
A zero-match result for any other collection most likely indicates a wrong
340+
GDC project ID in IDC_TO_GDC_PROJECTS.
341+
"""
342+
errors: list[str] = []
343+
for cid, group in studies_df.groupby("collection_id"):
344+
if cid in GDC_ABSENT_COLLECTIONS:
345+
continue
346+
if group["gdc_case_id"].notna().sum() == 0:
347+
errors.append(
348+
f" {cid}: 0/{len(group)} rows matched — check IDC_TO_GDC_PROJECTS"
349+
)
350+
if errors:
351+
msg = "Collections with no GDC matches (expected at least one):\n" + "\n".join(
352+
errors
353+
)
354+
raise RuntimeError(msg)
355+
356+
269357
def main() -> None:
270358
"""Main entry point."""
271359
project_id = os.environ.get("GCP_PROJECT")
@@ -298,6 +386,7 @@ def main() -> None:
298386
f" Patients: {n_patients_found}/{n_patients} unique (collection, PatientID) pairs in GDC"
299387
)
300388

389+
validate_gdc_matches(studies_df)
301390
save_results(studies_df)
302391

303392

0 commit comments

Comments
 (0)