Computes disproportionality signals — PRR, ROR, EBGM, and IC (BCPNN) — over FAERS / OpenFDA drug-event data to flag potential safety signals. Use when the user wants to mine spontaneous-report data for drug-reaction associations, build a 2x2 contingency table, compute a Proportional Reporting Ratio or Reporting Odds Ratio, run Empirical Bayes (EBGM/EB05) or Information Component shrinkage, or screen a drug for over-reported reactions. Trigger keywords: disproportionality, signal detection, PRR, ROR, EBGM, EB05, IC, BCPNN, MGPS, 2x2 table, signal of disproportionate reporting, SDR, OpenFDA, FAERS. Pairs adjacent to OpenMed: aggregate de-identified, coded cases (from reporting-adverse-events) then query the public OpenFDA /drug/event count API to build the contingency table. Reaction terms are MedDRA PTs (licensed, user-supplied).
73
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Spontaneous-report databases like the FDA's FAERS are mined for signals of disproportionate reporting (SDR): drug-reaction pairs that occur together more than expected given the background of all reports. The core device is a 2x2 contingency table and a disproportionality metric computed from it — PRR, ROR, EBGM, or IC (BCPNN).
You can build the 2x2 table directly from the public, free OpenFDA
/drug/event endpoint (no PHI, no MedDRA license to query; the reaction terms
returned are already MedDRA PTs). This skill is statistical screening: a high
PRR is a hypothesis, not a confirmed adverse drug reaction.
For one drug D and one reaction R, classify every report:
| Reaction R | Not R | |
|---|---|---|
| Drug D | a | b |
| Not D | c | d |
Common signal thresholds (screening only): PRR ≥ 2 with χ² ≥ 4 and a ≥ 3; ROR lower 95% CI > 1; IC025 > 0; EB05 ≥ 2.
Base endpoint: https://api.fda.gov/drug/event.json. No key needed to try it
(240 req/min, 1,000/day per IP; with a free api_key= key: 240/min,
120,000/day). The count=<field>.exact parameter returns a terms histogram, and
search= with +AND+ filters the population — that is all you need for a 2x2.
import requests
BASE = "https://api.fda.gov/drug/event.json"
def fda_count(search: str | None, count_field: str) -> int:
"""Total reports matching `search` (sum of the .exact histogram)."""
params = {"count": count_field}
if search:
params["search"] = search
r = requests.get(BASE, params=params, timeout=30)
if r.status_code == 404: # OpenFDA returns 404 for an empty result set
return 0
r.raise_for_status()
return sum(row["count"] for row in r.json()["results"])
def cell_count(search: str | None) -> int:
"""Number of reports matching `search` (use meta.results.total via limit=1)."""
params = {"limit": 1}
if search:
params["search"] = search
r = requests.get(BASE, params=params, timeout=30)
if r.status_code == 404:
return 0
r.raise_for_status()
return r.json()["meta"]["results"]["total"]
# Build the 2x2 for warfarin x "gastrointestinal haemorrhage".
DRUG = 'patient.drug.openfda.generic_name:"warfarin"'
RXN = 'patient.reaction.reactionmeddrapt.exact:"gastrointestinal haemorrhage"'
a = cell_count(f"{DRUG}+AND+{RXN}") # drug & reaction
b = cell_count(DRUG) - a # drug, not reaction
c = cell_count(RXN) - a # reaction, not drug
N = cell_count(None) # total reports in FAERS
d = N - a - b - cCompute the metrics from (a, b, c, d):
import math
def prr(a, b, c, d):
return (a / (a + b)) / (c / (c + d))
def ror(a, b, c, d):
return (a * d) / (b * c)
def ror_ci(a, b, c, d):
lnror = math.log((a * d) / (b * c))
se = math.sqrt(1/a + 1/b + 1/c + 1/d) # Woolf's method
lo, hi = math.exp(lnror - 1.96 * se), math.exp(lnror + 1.96 * se)
return lo, hi
def ic(a, b, c, d):
n = a + b + c + d
expected = (a + b) * (a + c) / n
return math.log2(a / expected) if a and expected else float("nan")
print("PRR", round(prr(a, b, c, d), 2))
print("ROR", round(ror(a, b, c, d), 2), "95% CI", ror_ci(a, b, c, d))
print("IC", round(ic(a, b, c, d), 2))For EBGM / EB05 use a maintained Empirical Bayes implementation (e.g. the
openEBGM R package or PhViD in R) on the same (a, b, c, d) rather than
hand-rolling the gamma-Poisson MGPS shrinkage — the shrinkage prior is the whole
point and easy to get wrong.
receivedate:[20230101+TO+20231231]). The choice of c/d defines the
"expected".patient.drug.openfda.generic_name (RxNorm
ingredient-normalized) over the free-text medicinalproduct to avoid brand
fragmentation. Restrict to suspect drugs with
patient.drug.drugcharacterization:1 if you want suspect-only signals..exact for the reaction field so "injection site reaction" counts as
one phrase, not three words: patient.reaction.reactionmeddrapt.exact.a + b + c + d == N.reporting-adverse-events: your own coded, de-identified ICSRs give
internal counts you can use instead of or alongside OpenFDA — the same
2x2 math applies. Aggregate only counts; never put narrative PHI in the table.normalizing-rxnorm: normalize the drug name to an RxNorm ingredient
before querying so brand/generic synonyms collapse to one cell.querying-openfda-labels: for every signal, check whether the reaction
is already on the label (expected) via /drug/label. To
reporting-adverse-events: a confirmed signal may require expedited reporting.a < 3 the ratios are unstable and CIs
explode. This is exactly why EBGM/EB05 and IC025 (shrinkage) exist —
prefer them for rare events..exact is mandatory for counting phrases. Without it, OpenFDA tokenizes
the reaction and your counts are wrong.count, .exact, search AND/OR): https://open.fda.gov/apis/query-syntax/80da98c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.