import type { Metadata } from 'next'; import Link from 'next/link'; import { PageHeader, Section } from '@/components/ui/section'; import { Badge, ClaimBadge } from '@/components/ui/badge'; import { EmptyState } from '@/components/ui/empty-state'; import { JsonView } from '@/components/ui/json-view'; import { listMetrics } from '@/lib/queries/rankings'; import { humanize, unitLabel } from '@/lib/format'; export const metadata: Metadata = { title: 'Methodology', description: 'How CancerIndex normalizes, reconciles, aggregates and ranks: every metric with its formula and version.' }; export const revalidate = 3600; const CATEGORY_LABEL: Record = { burden: 'Burden', lethality: 'Lethality', trials: 'Clinical research', research: 'Research activity', trend: 'Trends', molecular: 'Molecular knowledge', unmet_need: 'Gap indexes', rarity: 'Rarity', treatment: 'Treatment', composite: 'Composite' }; export default async function MethodologyPage() { const metrics = await listMetrics(); const cats = [...new Set(metrics.map((m) => m.category))]; return (

Every value belongs to one layer: raw (the payload as published, kept in the data lake), normalized (units, labels and identifiers harmonized; values unchanged), canonical (attached to a CancerIndex entity), derived (computed by a versioned formula) and ranked. AI-generated synthesis is a sixth layer that is not enabled in this phase.

Claims are labelled by kind and never merged:

  • Labels: Unicode NFKD, diacritics stripped, lower-case, punctuation to space, Greek letters spelled out (α → alpha), British/American spellings and plurals folded (tumour → tumor, carcinomas → carcinoma), "NOS" removed. Digits are kept (G12C, HER2).
  • MeSH inverted forms are un-inverted ("Carcinoma, Non-Small-Cell Lung" → "Non-Small-Cell Lung Carcinoma").
  • Rates keep the source's standard population; rates standardized to different populations are never placed in one ranking.
  • Observations are time-aware: a value for year X never overwrites year Y. Multi-year aggregates keep both bounds.
  • Variants keep original and normalized nomenclature, and every coordinate carries its assembly (GRCh37 / GRCh38).

Source labels are mapped to entities in a fixed order: shared identifiers, then curated aliases, then normalized strings. Language models are never the mapper; at most they propose candidates that a curator reviews. Unmatched labels go to a review queue and are never dropped. Every mapping stores one of these match types:

  • EXACT_IDENTIFIER shared code (NCIt, DOID, OncoTree…)
  • CURATED_EXACT one-to-one curated mapping · ONTOLOGY_EXACT exact via ontology cross-reference
  • CURATED_BROADER / CURATED_NARROWER curated mapping to a broader or narrower concept
  • ALIAS known alias after normalization · PROBABILISTIC string similarity, shown with caution · UNRESOLVED not mapped

Counts of trials, evidence items, cohorts and genes aggregate over an entity's descendants across all hierarchy types (a trial mapped to "Lung Adenocarcinoma" counts for "Lung Cancer"). Because descendants overlap between branches, these counts are only compared within one entity level. Global burden rankings use a mutually exclusive top-level set of 36 registry site groups (GLOBOCAN / ICD-10 ranges anchored to NCIt concepts), so that no case is counted twice.

  • Confidence intervals are shown exactly as published; CancerIndex does not compute new intervals.
  • Per-row confidence: High observed registry data with complete coverage · Medium estimated or modelled · Low small denominators or indirect mapping · Insufficient data.
  • Estimate type (observed / estimated / projected) is displayed on every observation.
  • Population survival statistics never predict individual outcomes and are shown with that note.
{metrics.length === 0 ? ( ) : (
{cats.map((cat) => (

{CATEGORY_LABEL[cat] ?? humanize(cat)}

{metrics .filter((m) => m.category === cat) .map((m) => (

{m.name} {m.slug} {m.experimental ? experimental : null} {m.snapshot_count > 0 ? ( view ranking ) : ( not yet computed )}

{m.description}

Formula
{m.formula}
Version
{m.formula_version}
Unit
{unitLabel(m.unit)}
Direction
{m.higher_is_worse == null ? 'neutral' : m.higher_is_worse ? 'higher is worse' : 'higher is better'}
Aggregation
{m.aggregation ?? 'none'}
Dimensions
{m.valid_dimensions.join(', ')}
Sources
{m.source_slugs.map((s) => ( {s} ))}
{Object.keys(m.eligibility ?? {}).length ? ( <>
Eligibility
) : null}
))}
))}
)}

A ranking snapshot is one metric × one scope × one formula version at one time. It stores the number of eligible entities, the list of source ids and a hash of its inputs; each row stores its lineage (observation ids, counters, formula inputs). Re-running the engine with identical inputs yields identical ranks. Changing a formula creates a new version; old snapshots remain queryable. Public identifiers (CI-CAN-…, CI-GENE-…) are minted once and never reused.

Population statistics are stored per geography, year, sex and age group, exactly as published (), and rankings are computed per scope key — geo=<ISO3>|sex=<all|male|female>|age=all|year=<YYYY>|level=top. A country page (/countries, /country/united-states) exists only for geographies with at least one ingested observation; nothing is shown for the others.

  • United States — U.S. Cancer Statistics (CDC): NPCR/SEER incidence and NVSS mortality, 1999 onwards, per USCS site group (ICD-O-3 / ICD-10 ranges shown as the site definition on every row), both sexes and by sex, all ages, age-standardized to the 2000 U.S. standard population (19 age groups). Incidence is published about one year later than mortality, so the latest year differs between metrics; the page states the year actually used. USCS site groups are mapped one-to-one to the top-level registry set; "Female Breast" / "Male Breast" / "Male and Female Breast" map to the same entity and are selected by the sex dimension.
  • Why US only for now — the connectors that would cover other countries are not licensed for production: the IARC Global Cancer Observatory (GLOBOCAN, 185 countries) has license status review and the SEER API is awaiting credentials (CLAUDE.md §10.4, §142). Estimates are never substituted for registry counts, and no country is listed without observations.
  • All-sites totals — a national total is displayed only when the source publishes an all-sites row. CancerIndex never sums site groups (definitions overlap across sources and years); when the row is absent the page says so and shows per-site figures only.
  • Standard populations — rates standardized to the 2000 U.S. standard population are not comparable with World (Segi) standardized rates used by IARC; the standard is printed with every rate and rankings never mix standards.

The home module computes, for each cancer with a complete series, the compound annual growth rate of the age-standardized incidence rate over the last ten years published for the geography (both sexes, all ages):

CAGR = (ASIR[end_year] / ASIR[start_year])^(1 / (end_year − start_year)) − 1

  • Window: the ten most recent distinct years with observations; the module is hidden when fewer than ten years exist. Cancers lacking a value at either endpoint, or with fewer than ten points in the window, are skipped — nothing is interpolated.
  • Inputs: the two endpoint observations only (their provenance is kept on the row); the value is labelled with version ci-asir-cagr-10y-v1. It is a page-level derivation, not a stored metric and not a ranking snapshot.
  • Interpretation: a rising rate may reflect true risk change, screening or diagnostic practice, coding or registry changes, or residual demographic effects. The module states the two endpoint values so the number can be reproduced.
  • Two to four cancers are compared (/compare?ids=slug-1,slug-2); the selection lives in the URL so a comparison is shareable. Unknown or merged slugs are ignored and reported.
  • Nearest top-level ancestor — registry figures exist only for the mutually exclusive top-level site groups. For an entity below that level (e.g. glioblastoma), the compare page and the overview "Key figures" strip walk every hierarchy type upwards and use the nearest active top-level ancestor (e.g. "Malignant Central Nervous System Neoplasm"), labelling the cell "figures for <ancestor> (registry level)". Those figures describe the whole site group, not the subtype. When no top-level ancestor exists the cell reads "no registry observation at this level".
  • Counters (trials, publications, evidence, genes, cohorts, drugs) are shown for the entity itself and aggregate over its descendants; entities at different depths are therefore labelled with their depth and hierarchy position and should not be read as equivalent.
  • Ranks show the entity's own current rank when one exists (top-level entities among the top-level set, others among all malignant entities), otherwise the ancestor's burden rank, labelled as such. Bars in the charts are proportional within one chart only; missing values render as a dash, never as zero.

Gap indexes are burden percentile − activity percentile within one scope (geography, year, sex, top level): trial_gap = percentile(mortality_count) − percentile(active_trials) and research_gap = percentile(mortality_count) − percentile(publications_5y). Positive values flag cancers with high mortality burden and comparatively little registered activity. They are quantitative signals, not accusations.

  • Trial counts aggregate a cancer and its NCIt descendants only. Registrations phrased at a broader level (e.g. "colorectal cancer" for colon and rectal cancer, or "head and neck cancer" for laryngeal and pharyngeal sites) are attributed to the broader entity, which can overstate the gap of narrower top-level sites.
  • Literature counts are query-based per entity (the query is stored with each count) and are not aggregated over descendants; entities without a stored query are excluded from the research gap.
  • Percentiles depend on the eligible set of the scope; a gap value is therefore comparable only with rows of the same snapshot. The number of eligible entities and the two percentiles are stored on every row and shown on the ranking page.
  • Burden here is deaths in one geography (currently the United States); a cancer that is common elsewhere but rare in that geography will show a small burden percentile.

For one burden scope (geography, year, sex, one epidemiology source) and the eligible top-level cancers (annual deaths ≥ 100), each cancer's share of deaths is compared with its share of active interventional trials and its share of publications over 5 years, all shares summed over the same eligible set. trial_gap_ratio = log₂(death share / trial share), research_gap_ratio = log₂(death share / publication share); 0 means the activity share matches the death share, +1 twice as many deaths as the activity would suggest, −1 half. The ratios are scale-free, unlike the percentile indexes above, so they can be read across scopes. The same rows carry trials_per_1000_deaths and publications_per_1000_deaths. A ratio is undefined (not zero) when the activity is 0.

  • Components are stored per cancer and scope in research_gap_components (formula ci-research-gap-components-v1) with the observation ids, counters and sums that produced them; ranking snapshots trial_gap_ratio, research_gap_ratio, trials_per_1000_deaths, publications_per_1000_deaths are persisted for the preferred source of each scope key.
  • Caveats are those of the percentile indexes (descendant aggregation of trials, query-based literature counts, US-only burden while GLOBOCAN is under review) plus a new one: narrow literature queries make small entities look under-published. Read it as a signal for further inquiry, never as a judgement of research quality or funding.

Open the Research Gap page · full method: docs/methodology/research-gap.md.

trial_intelligence (formula ci-trial-intel-v1) is rebuilt daily for every top-level cancer and every malignant entity with at least one mapped study. Studies attach to a cancer through their reconciled conditions over the NCIt descendants (depth ≤ 12, distinct studies); counts use interventional studies and the same active statuses as the counters (recruiting, not yet recruiting, enrolling by invitation, active not recruiting).

  • Growth: studies first posted in the last 12 months versus the preceding 12 months; the year-over-year ratio is shown only when the prior window holds ≥ 20 studies.
  • Sponsor and country concentration: Herfindahl–Hirschman index (sum of squared shares) over active studies, by lead sponsor and by study–country pairs (a multinational study contributes to every country it lists); HHI needs ≥ 10 active studies. Industry share = lead sponsor class INDUSTRY; US share = studies listing a US site.
  • Termination share: (terminated + withdrawn) / (completed + terminated + withdrawn) over studies first posted since 2010, shown when the denominator is ≥ 30. Stated reasons are classified by explicit keyword rules (ci-stop-reasons-v1; precedence covid › safety › efficacy › drug supply › investigator › enrollment › funding › sponsor decision; text without a match is "other stated"; empty is "not stated") and never inferred.
  • Burden-normalized intensity: active studies per 1,000 annual deaths and per 100,000 new cases, using the latest US year with both counts from one source (deaths ≥ 100); trial counts are worldwide while the burden is US, which the table states.
  • Four rankings derive from this layer: phase3_recruiting_trials, trial_growth_yoy, trial_termination_share, sponsor_concentration.

Trial intelligence table · Terminated studies · full method: docs/methodology/trial-intelligence.md.

trial_site_country_counts (formula ci-trial-sites-v1) counts registrant-entered study locations per country for all oncology studies and for each top-level cancer (descendants included), any phase or one phase (a Phase II/III study counts in both), all statuses or recruiting only (location status when present, otherwise the study status). A study with forty US sites weighs forty sites and one study. Country names are mapped to ISO 3166-1 alpha-3 by a tested table; historical names that no longer exist stay unmapped and are listed, not painted. Fill classes are quantiles of the displayed values and the legend states the breaks; the data table below the map is the reference.

Open the map · detailed method page.

An approval record is one authority's decision for one application or product identifier, with jurisdiction, date, status and the indication text as published — never a bare "approved". US records come from openFDA (Drugs@FDA applications and labels); Canadian records come from the Health Canada Drug Product Database (one record per DIN with ATC class, market date and product status; the DPD does not publish indications, so no cancer is attached to these records and none is guessed from the ATC class). drug_pipeline (formula ci-drug-pipeline-v1) then assigns each drug — overall and per top-level cancer reached through trial conditions or approvals — a stage: approved when any approval is current; withdrawn when approvals exist but none is current; otherwise the highest registry phase among its interventional studies (Phase II/III → Phase III), or "phase not stated". Duplicate drug entities (salt forms, shared codes) are proposed to a merge queue, never merged automatically.

Approvals feed · Pipeline · full method: docs/methodology/pipeline.md.

A biomarker entry is curated metadata: 54 canonical oncology biomarkers (HER2, EGFR mutations, ALK/ROS1/RET/NTRK fusions, KRAS G12C, BRAF V600E, PD-L1, MSI-H/dMMR, TMB-H, BRCA1/2, HRD, PSMA, CD19, BCMA…), each with its kind, anchor gene(s), an NCIt biomarker concept verified against the NCI EVS API (version and date shown on the page), assay families and scoring conventions with source links. Every link shown on a biomarker page is derived at query time from the anchor gene(s) — CIViC evidence grouped by cancer with native levels, drugs with predictive evidence and their direction, jurisdiction-aware approvals for those drugs, active studies of those drugs in the associated cancers, publications — under the rule biomarker-links-v1; nothing is asserted about clinical utility beyond the source-native evidence levels. Tumor-agnostic status is shown from actual approval rows flagged as such, never from the curated list alone.

Biomarker table · full method: docs/methodology/biomarkers.md.

The graph page shows the neighbourhood of one entity. Two families of links are never merged: source-native edges from knowledge_edges (CIViC, ChEMBL, openFDA…) with their native relationship, direction, evidence level, cancer context and provenance, aggregated for display per neighbour, relationship, direction, level and source; and derived registry links (dashed) that are counts computed at query time — studies per cancer, alteration frequency per cohort (≥ 5 %, ≥ 20 cases, largest denominator), approvals, drugs investigated in trials. At most 60 nodes are drawn, 25 per relationship group unless expanded; the table under the graph is complete for the expanded groups. CancerIndex never infers an edge and no language model writes into the graph.

Open the graph · full method: docs/methodology/knowledge-graph.md.

Observations are only overlaid on one chart when they share metric, unit, geography, source, standard population and age group; otherwise they are drawn as separate charts with a caption explaining the split (for example two registries publishing deaths for the same year, or two standard populations). Series are one cancer × one sex, dashed when the source labels the value as estimated. Defaults (metric, geography, the five cancers shown) are computed from the data present, not curated. Every view has a permalink, a CSV with attribution rows and the equivalent API call, and the observations table lists each value with its year, geography, sex, age, confidence interval, estimate type, site definition and source.

Open the Data explorer · Coverage matrix · full method: docs/methodology/data-explorer.md.

  • Burden, lethality and gap rankings exist only for scopes with licensed registry observations — currently the United States (CDC U.S. Cancer Statistics), per year and sex, top level. Global scopes wait for IARC / GLOBOCAN (license review) and SEER (credentials); 5-year survival waits for survival observations.
  • Composite scores (an overall "impact" index) are deliberately absent: they would hide the choice of weights.
  • AI-generated summaries are not enabled; all text on entity pages is sourced from terminologies or written as fixed methodology.
  • Registries differ in site definitions (e.g. non-melanoma skin cancer inclusion) and in completeness; site definitions are shown per observation.
  • Trial conditions and publication links rely on reconciliation; probabilistic matches are labelled and candidate links are not counted as validated.
  • Literature counts measure indexed records returned by a stored query, not scientific quality or clinical impact.
  • Counts aggregate over descendants; entities at different hierarchy depths are not directly comparable.
  • Approval records reflect the jurisdictions ingested; absence of an approval on this site is not evidence of absence.

Corrections: contact@spboucher.ai. See also Trust & policies.

); }