SPB Git

spb/cancerindex

Public

Resolver: byCode carries recorded match types (alias/lineage codes); MeSH/ChEMBL docs

Simon-Pierre Boucher committed (Sep 8, 2026) parent 2663b65 Browse files

2 changed files +33 −7

modified docs/connectors/chembl.md +19 −2
@@ -60,9 +60,26 @@ Search pages (osimertinib, imatinib, trastuzumab, empty, page with a malformed m
60 60
61 ## Observed run (cancerindex_c, 2026-09-08, `--max-minutes 15`, 661 CIViC drugs) 61 ## Observed run (cancerindex_c, 2026-09-08, `--max-minutes 15`, 661 CIViC drugs)
62 62
63−See the table below — filled from the run log of `ING-CHEMBL-20260908-*`. 63+The EBI API was unstable during the day (HTTP 500 / hung connections for ~40 min; 16 of 2,622 requests failed after retries): the first 15-minute run processed only 14 drugs (each hung request costs the SDK's 60 s timeout × retries). Once the API recovered the resumable cursor was used to cover the whole list in segments (10 runs, 62 min wall-clock, ~1.3 s per drug when healthy — a healthy full pass of 661 drugs takes ≈ 12 min).
64 64
65−OBSERVED_RUN_TABLE 65+| | |
66+|---|---|
67+| Drugs considered | 661 (every drug processed at least once across the segments); 66 skipped as non-molecule names (regimens, combinations, procedures) |
68+| Matched (exact name) | **401 drugs enriched with a ChEMBL id** (≈ 91 % by `pref_name`, 9 % by synonym); 190 names unresolved → `unresolved_labels` (`entity_kind drug`, with the top hits) |
69+| `kind` | small_molecule 342, monoclonal_antibody 36, adc 10, other 7, gene_therapy 2, cell_therapy 1 (3 kept null — ChEMBL `Unknown`) |
70+| `development_status` | approved 223, phase_3 73, phase_2 67, phase_1 9 (29 without a ChEMBL phase) |
71+| `mechanism` / `target_gene_ids` | 339 drugs with mechanism text; 314 drugs with ≥ 1 target gene |
72+| `drug_aliases` | 2,830 — development_code 1,192, synonym 847, brand 741, generic 50 |
73+| drug **TARGETS** gene | **1,171 edges** (314 drugs → 349 genes), `evidence_level` = ChEMBL action type |
74+| drug **INVESTIGATED_FOR** cancer | **6,545 edges** (361 drugs → 218 cancers), `evidence_level` = max phase for the indication |
75+| Indication headings not resolved | 99 distinct MeSH headings queued as unresolved cancers (e.g. Prostatic Neoplasms, Castration-Resistant [D064129] 41×, Neoplasm Metastasis, Adenocarcinoma, Gliosarcoma, Lymphoma Large B-Cell Diffuse, Glioma, Neuroblastoma) — descriptors the `mesh` connector could not map (genuine gaps or, on this development database, headings ambiguous because OncoTree minted duplicates before NCIt ran); non-cancer indications (rheumatoid arthritis, malaria, pulmonary hypertension…) are counted, not queued |
76+| Samples | **Osimertinib** → CHEMBL3353410, small_molecule, approved, mechanism "Epidermal growth factor receptor erbB1 inhibitor", TARGETS EGFR (INHIBITOR), aliases Tagrisso (brand), AZD-9291 / AZD9291 (development_code), INVESTIGATED_FOR Lung Non-Small Cell Carcinoma (4), Lung Neoplasm (3), Lung Adenocarcinoma (2), Glioblastoma (2)… — **Imatinib** → CHEMBL941, mechanism "Tyrosine-protein kinase ABL inhibitor; Platelet-derived growth factor receptor beta inhibitor; Stem cell growth factor receptor inhibitor; Bcr/Abl fusion protein inhibitor", TARGETS ABL1, KIT, PDGFRB, BCR, INVESTIGATED_FOR Gastrointestinal Stromal Tumor (4), Chronic Myeloid Leukemia BCR-ABL1 Positive (4), Acute Lymphoblastic Leukemia (4), Dermatofibrosarcoma Protuberans (4)… — **Trastuzumab** → CHEMBL1201585, monoclonal_antibody, approved, "Receptor protein-tyrosine kinase erbB-2 inhibitor", TARGETS ERBB2, INVESTIGATED_FOR Breast Neoplasm (4), Gastric Neoplasm (4), Esophageal Neoplasm (3)…; Trastuzumab Deruxtecan → adc, TARGETS ERBB2 + TOP1 |
77+
78+Idempotency: re-processing a drug reports the source record `unchanged`, reuses its provenance row and refreshes edges in place. Note for the integrator: CIViC created two `drugs` rows named "Trastuzumab Deruxtecan"; both received the same ChEMBL id (a merge candidate).
79+
80+### Resolver note
81+
82+`CancerResolver.byCode()` only caches codes with match type EXACT_IDENTIFIER / CURATED_EXACT / ONTOLOGY_EXACT. The `mesh` connector also writes deliberate `ALIAS` (entry-term) and `CURATED_BROADER` codes (206 of 428), so the connector loads `cancer_codes system 'mesh'` itself (all match types, ambiguous UIs dropped) and uses the resolver as the first lookup. Widening the resolver's filter to `ALIAS` would make `byCode('mesh', …)` complete for every consumer (PubMed currently falls back to `byLabel` on the `mesh_heading` alias, so its edges are unaffected).
66 83
67 ## Limitations / notes 84 ## Limitations / notes
68 85
modified packages/ontology/src/reconcile.ts +14 −5
@@ -16,7 +16,7 @@ export interface CancerMatch {
16 */ 16 */
17 export class CancerResolver { 17 export class CancerResolver {
18 private aliasCache = new Map<string, { cancerId: string; aliasType: string }[]>(); 18 private aliasCache = new Map<string, { cancerId: string; aliasType: string }[]>();
19− private codeCache = new Map<string, string>(); 19+ private codeCache = new Map<string, { cancerId: string; matchType: MatchType }>();
20 20
21 constructor(private readonly db: Database) {} 21 constructor(private readonly db: Database) {}
22 22
@@ -33,9 +33,16 @@ export class CancerResolver {
33 arr.push({ cancerId: r.cancerId, aliasType: r.aliasType }); 33 arr.push({ cancerId: r.cancerId, aliasType: r.aliasType });
34 this.aliasCache.set(r.normalized, arr); 34 this.aliasCache.set(r.normalized, arr);
35 } 35 }
36− const codes = await this.db.select({ cancerId: cancerCodes.cancerId, system: cancerCodes.system, code: cancerCodes.code }).from(cancerCodes).where(inArray(cancerCodes.matchType, ['EXACT_IDENTIFIER', 'CURATED_EXACT', 'ONTOLOGY_EXACT'])); 36+ // Codes recorded through an alias or a lineage decision are still identifiers of that entity; the
37+ // stored match type is carried through so callers can judge confidence (exact codes win on conflict).
38+ const codes = await this.db.select({ cancerId: cancerCodes.cancerId, system: cancerCodes.system, code: cancerCodes.code, matchType: cancerCodes.matchType }).from(cancerCodes).where(inArray(cancerCodes.matchType, ['EXACT_IDENTIFIER', 'CURATED_EXACT', 'ONTOLOGY_EXACT', 'ALIAS', 'CURATED_BROADER']));
37 this.codeCache.clear(); 39 this.codeCache.clear();
38− for (const c of codes) this.codeCache.set(`${c.system}:${c.code}`, c.cancerId); 40+ const rank: Record<string, number> = { EXACT_IDENTIFIER: 5, CURATED_EXACT: 4, ONTOLOGY_EXACT: 3, ALIAS: 2, CURATED_BROADER: 1 };
41+ for (const c of codes) {
42+ const key = `${c.system}:${c.code}`;
43+ const cur = this.codeCache.get(key);
44+ if (!cur || (rank[c.matchType] ?? 0) > (rank[cur.matchType] ?? 0)) this.codeCache.set(key, { cancerId: c.cancerId, matchType: c.matchType as MatchType });
45+ }
39 const edges = await this.db.select({ parentId: cancerHierarchy.parentId, childId: cancerHierarchy.childId }).from(cancerHierarchy); 46 const edges = await this.db.select({ parentId: cancerHierarchy.parentId, childId: cancerHierarchy.childId }).from(cancerHierarchy);
40 this.parents.clear(); 47 this.parents.clear();
41 for (const e of edges) { 48 for (const e of edges) {
@@ -83,8 +90,10 @@ export class CancerResolver {
83 90
84 /** Resolve by external identifier (NCIt code, DOID, OncoTree code, UMLS CUI, MeSH UI…). */ 91 /** Resolve by external identifier (NCIt code, DOID, OncoTree code, UMLS CUI, MeSH UI…). */
85 byCode(system: string, code: string): CancerMatch | null { 92 byCode(system: string, code: string): CancerMatch | null {
86− const id = this.codeCache.get(`${system}:${code}`); 93+ const hit = this.codeCache.get(`${system}:${code}`);
87− return id ? { cancerId: id, matchType: 'EXACT_IDENTIFIER', confidence: 1, via: `${system}:${code}` } : null; 94+ if (!hit) return null;
95+ const confidence = hit.matchType === 'ALIAS' ? 0.9 : hit.matchType === 'CURATED_BROADER' ? 0.8 : 1;
96+ return { cancerId: hit.cancerId, matchType: hit.matchType, confidence, via: `${system}:${code}${hit.matchType === 'EXACT_IDENTIFIER' ? '' : ` (code recorded as ${hit.matchType})`}` };
88 } 97 }
89 98
90 /** Resolve a free-text label. Ambiguous alias hits (≠ 1 distinct cancer) are returned as null. */ 99 /** Resolve a free-text label. Ambiguous alias hits (≠ 1 distinct cancer) are returned as null. */
91 100
Menu

Theme