Skip to content
Security
Skill

/detecting-pv-signals

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

BOOST
From plugin
openmed
5.7k74 skills
Install
$ npx -y skills add maziyarpanahi/openmed --skill detecting-pv-signals --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/detecting-pv-signals

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

detecting-pv-signals.SKILL.md
name: detecting-pv-signals
description: "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)."
license: Apache-2.0
metadata:
  project: OpenMed
  category: safety-pharmacovigilance
  pairs: adjacent
  version: "1.0"

Detecting pharmacovigilance signals (disproportionality)

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.

When to use

  • You have a drug of interest and want to see which reactions are over-reported.
  • You need a PRR / ROR with confidence interval, or an Empirical Bayes EBGM/EB05

/ IC025 to control for the small-count noise PRR/ROR suffer from.

  • You are building a routine signal-screening run over OpenFDA or your own

aggregated case counts.

The 2x2 table

For one drug D and one reaction R, classify every report:

| | Reaction R | Not R | | ---------- | ---------- | ----- | | Drug D | **a** | **b** | | Not D | **c** | **d** |

  • **PRR** = [a/(a+b)] / [c/(c+d)]
  • **ROR** = (a·d)/(b·c)
  • **IC** (BCPNN, log2 information component) ≈ log2( a·(a+b+c+d) / ((a+b)·(a+c)) )
  • **EBGM** = Empirical Bayes Geometric Mean — a gamma-Poisson *shrinkage* of the

observed/expected ratio (the MGPS method) that pulls small-count estimates toward 1; report **EB05** (the 5th percentile) as the conservative signal.

Common signal thresholds (screening only): PRR ≥ 2 with χ² ≥ 4 and a ≥ 3; ROR lower 95% CI > 1; **IC025 > 0**; **EB05 ≥ 2**.

Quick start (real OpenFDA count queries)

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 - c

Compute 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.

Workflow

1. **Pick the population.** Decide your denominator: all of FAERS, or a restricted background (e.g. one drug class, one year via `receivedate:[20230101+TO+20231231]`). The choice of `c`/`d` defines the "expected". 2. **Resolve the drug field.** Prefer `patient.drug.openfda.generic_name` (RxNorm ingredient-normalized) over the free-text `medicinalproduct` to avoid brand fragmentation. Restrict to suspect drugs with `patient.drug.drugcharacterizati

Read more
Ships withopenmed

Local-first healthcare AI: clinical NER and HIPAA PII de-identification on hardware you control. 2,200+ medical models, 35 model-backed PII languages, and Python, MLX, Android and browser runtimes. Apache-2.0 SDK.

Get the whole plugin

Other skills on openmed.