SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%

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

Simon-Pierre Boucher committed 16 days ago (Sep 8, 2026) parent 2663b65

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 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 84 ## Limitations / notes
68 85
modified packages/ontology/src/reconcile.ts +14 −5
@@ -16,7 +16,7 @@ export interface CancerMatch {
16 16 */
17 17 export class CancerResolver {
18 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 21 constructor(private readonly db: Database) {}
22 22
@@ -33,9 +33,16 @@ export class CancerResolver {
33 33 arr.push({ cancerId: r.cancerId, aliasType: r.aliasType });
34 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 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 46 const edges = await this.db.select({ parentId: cancerHierarchy.parentId, childId: cancerHierarchy.childId }).from(cancerHierarchy);
40 47 this.parents.clear();
41 48 for (const e of edges) {
@@ -83,8 +90,10 @@ export class CancerResolver {
83 90
84 91 /** Resolve by external identifier (NCIt code, DOID, OncoTree code, UMLS CUI, MeSH UI…). */
85 92 byCode(system: string, code: string): CancerMatch | null {
86 − const id = this.codeCache.get(`${system}:${code}`);
87 − return id ? { cancerId: id, matchType: 'EXACT_IDENTIFIER', confidence: 1, via: `${system}:${code}` } : null;
93 + const hit = this.codeCache.get(`${system}:${code}`);
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 99 /** Resolve a free-text label. Ambiguous alias hits (≠ 1 distinct cancer) are returned as null. */
91 100