# Research Gap Index — methodology *Formula versions: components `ci-research-gap-components-v1`; rankings `ci-trial-gap-ratio-v1`, `ci-research-gap-ratio-v1`, `ci-trials-per-deaths-v1`, `ci-pubs-per-deaths-v1`. Code: `packages/ranking/src/research-gap.ts` (unit tests in `packages/ranking/test/research-gap.test.ts`). Table: `research_gap_components`. Web: `/research-gap`, `/rankings/trial_gap_ratio`, `/rankings/research_gap_ratio`. API: `GET /v1/research-gap`, `GET /v1/research-gap/scopes`.* ## What it measures The Research Gap Index compares, inside **one burden scope**, each top-level cancer's **share of deaths** with its **share of registered research activity**: - **active interventional trials** registered on ClinicalTrials.gov and mapped to the cancer or one of its NCIt descendants (`entity_counters.active_trial_count`); - **publications of the last five years** in PubMed (`entity_counters.publication_count_5y`, falling back to `literature_counts` window `5y` when the counter is empty; the exact query is stored on the literature row and copied into `inputs`). It is a **computed metric** (claim category `computed_metric`). It says *where registered activity is thin relative to mortality*. It does **not** say anything about the quality, cost, difficulty or funding of research, and it inherits every limitation of the burden source (site definitions, coding, completeness). ## Scope A scope is a (geography, year, sex, burden source) combination in which at least **10 top-level cancers** carry a `mortality_count` observation with `age_group = 'all'` — exactly the discovery rule of the ranking engine (`packages/ranking/src/engine.ts`). Every sex present is a separate scope (`all`, `male`, `female`). Scopes are rebuilt from scratch on each run (`pnpm cix intel`): the rows of a scope are deleted and reinserted in one transaction, then four ranking snapshots are persisted through the shared `persistSnapshot` so "Why this rank?", previous-rank deltas, TRACE and CSV exports work like for any other metric. Burden today is **United States only** (CDC WONDER underlying-cause mortality; U.S. Cancer Statistics NVSS mortality). IARC / GLOBOCAN is under license review, so no `WORLD` scope exists; a US death share must not be read as a global death share. ## Eligibility | Rule | Effect | |---|---| | `deaths ≥ 100` in the scope (`eligibility.minDeaths` of the four metric definitions) | the cancer enters the **eligible set** `E`; below the threshold the row is stored with `eligible = false` and `ineligible_reason = "deaths_below_threshold (n < 100)"`, shares and ratios `NULL` | | no mortality observation | not stored (the scope is built from mortality observations) — the reason `no_mortality_observation` exists in the pure function for completeness | | `active_trials ≥ 1` | needed for `trial_gap_ratio` (log₂ undefined at 0); otherwise that ratio is `NULL`, the row stays eligible and its 0 trials still count in Σ | | `publications_5y ≥ 1` | same for `research_gap_ratio` | Per-1,000-deaths intensities are computed for every stored row (they need no cross-entity denominator) but only eligible rows are ranked. Ranking confidence is `HIGH` when `deaths ≥ 1000`, `MEDIUM` otherwise. Sex-specific scopes naturally produce zero-death rows (e.g. prostate in the female scope): they are stored as ineligible, never dropped. ## Shares All sums run over the eligible set `E` of the scope: ``` death_share_i = deaths_i / Σ_{j∈E} deaths_j trial_share_i = active_trials_i / Σ_{j∈E} active_trials_j publication_share_i = publications_5y_i / Σ_{j∈E} publications_5y_j ``` Shares are stored rounded to 6 decimals; the ratios below are computed from the unrounded shares (so rounding never leaks into the index). Each share column sums to 1 over `E`. ## Ratios and intensities ``` trial_gap_ratio_i = log2( death_share_i / trial_share_i ) research_gap_ratio_i = log2( death_share_i / publication_share_i ) trials_per_1000_deaths_i = active_trials_i / (deaths_i / 1000) publications_per_1000_deaths_i = publications_5y_i / (deaths_i / 1000) ``` Ratios are rounded to 6 decimals, intensities to 4. Rankings sort **descending**: rank 1 = largest positive gap. ### Why log₂ The raw quotient `death_share / activity_share` is asymmetric: "twice the share" is 2 but "half the share" is 0.5, so under- and over-representation of the same magnitude look very different and cannot be averaged or compared symmetrically. Taking log₂ makes the scale **symmetric around 0** and **scale-free**: it does not depend on the size of the scope, so a value of +1 in the US 2024 scope means the same thing as +1 in any other scope (unlike percentile-point differences, whose meaning depends on how many entities are ranked). Base 2 was chosen for readability: each unit is a doubling. ### How to read ±1 | Value | Reading | |---|---| | `0` | the cancer's share of trials (or publications) equals its share of deaths | | `+1` | it carries **twice** the share of deaths that its share of activity would suggest — half the activity per death of the scope average | | `+2` | four times | | `−1` | its share of activity is twice its share of deaths — twice the activity per death of the scope average | | `NULL` | undefined: no registered activity (`activity < 1`) or ineligible row | Equivalently, the ratio is `log2( scope-average intensity / own intensity )`: on the `/research-gap` scatter plots the dashed line is the scope's overall activity per death, and every cancer below it has a positive ratio. A change of one cancer's inputs changes every other cancer's ratio slightly, because the sums move. ## Inputs stored with every row `research_gap_components.inputs` (jsonb) records the mortality observation id (and any duplicate observation ids for the same cancer/metric in the scope, e.g. other site definitions), the incidence observation id when present, the counters' `updated_at`, which source supplied the publication count, the literature count id and query when the fallback was used, the thresholds, the per-ratio eligibility flags, the scope sums and the activity source ids. Ranking rows repeat deaths, the activity, both shares, the sums, the observation id and the formula text, so `/rankings/` can answer "Why this rank?" without joining back. ## Caveats 1. **Descendant aggregation of trials.** Trial counts include the cancer's NCIt descendants. A trial registered for "colorectal cancer" is attributed to the broader entity, not split between colon and rectum: narrower top-level sites can look under-trialled. 2. **Query-based literature counts.** Publication counts come from a PubMed query per entity (stored on the row) and are *not* aggregated over descendants. A narrow or unlucky query yields a small count and a large positive Research Gap Ratio — a very high ratio should first prompt a check of the query. 3. **Registrants' condition phrasing.** Whether a trial reaches a cancer depends on how sponsors phrase conditions and on reconciliation; unresolved labels are queued, not guessed. 4. **US-only burden.** The share of deaths is a share of *US* deaths in one year and one source. The same cancer can have a different ratio under another source (`cdc-wonder` vs `cdc-uscs` differ in site definitions and coverage) or another year. Compare only within one table. 5. **Correlation ≠ under-funding.** A positive ratio does not establish neglect: some cancers are hard to enrol, some are dominated by prevention or surgery rather than drug trials, some have large registries elsewhere. It is a signal, phrased as such ("signal, not accusation"). 6. **Relative to the eligible set.** Adding a cancer to the top-level set, or a cancer crossing the 100-deaths threshold, shifts every share. ## Differences from the percentile indexes `trial_gap` / `research_gap` (`ci-trial-gap-v1`, `ci-research-gap-v1`) are `percentile(mortality_count) − percentile(activity)`: rank-based, bounded to ±100 percentile points, insensitive to magnitudes (a 2× and a 20× mismatch can score the same) and dependent on the number of ranked entities. The ratio indexes are **magnitude-based, symmetric and scale-free**; they expose their inputs (shares, sums) directly and add the two intensities (`trials_per_1000_deaths`, `publications_per_1000_deaths`) that need no cross-entity comparison. Both families stay available: the percentile indexes for continuity, the ratio indexes as the primary reading on `/research-gap`. ## Recompute ``` pnpm cix counters # refresh entity_counters pnpm cix intel # research_gap_components + the four ranking snapshots (with the other intelligence tables) ```