Open-source pharmacovigilance opencore library for FDA FAERS adverse-event signal detection via the openFDA API.
@faerscope/opencore provides the core statistical and data-retrieval primitives for building drug safety signal detection tools on top of the FDA's Adverse Event Reporting System (FAERS). It is the open-source foundation of FAERScope, extracted as a standalone package under the Apache-2.0 license.
This library is designed for:
- Pharmacovigilance researchers building custom FAERS analysis pipelines
- Health data scientists who need validated disproportionality calculations
- Open-source contributors building pharmacoepidemiology tools
- Educators teaching signal detection methodology
Important: Disproportionality metrics quantify statistical association in spontaneous reports. They do not establish causation, incidence rates, or comparative safety. Always interpret results in clinical context. See Limitations.
npm install @faerscope/opencorepnpm add @faerscope/opencoreyarn add @faerscope/opencoreimport {
fetchTrend,
fetchTopReactions,
computeDisproportionality,
applyBenjaminiHochberg,
getSoc,
titleCaseTerm,
} from "@faerscope/opencore";
// 1. Fetch trend data from openFDA
const trend = await fetchTrend({
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
});
// 2. Get top adverse reactions
const reactions = await fetchTopReactions(
{ drug: "metformin", dateFrom: "20200101", dateTo: "20231231" },
20
);
// 3. Compute disproportionality for a reaction
// (You need to build the 2x2 table from openFDA counts)
const table = { a: 150, b: 4850, c: 3000, d: 992000, n: 1000000 };
const score = computeDisproportionality("NAUSEA", 150, table);
console.log(score.prr); // 10.0
console.log(score.isSignal); // true (Evans criteria)
console.log(score.isStrongSignal); // true (ROR CI > 1, IC025 > 0)
// 4. Apply FDR correction across all tested reactions
const corrected = applyBenjaminiHochberg([score, /* ...more scores */]);
// 5. Map reactions to organ classes
console.log(getSoc("NAUSEA")); // "Gastrointestinal disorders"
console.log(titleCaseTerm("NAUSEA")); // "Nausea"| Module | Description |
|---|---|
disproportionality |
PRR, ROR, IC, Chi-squared, BH-FDR, time-series statistics |
openfda |
Lightweight openFDA fetch utilities |
soc-mapping |
MedDRA Preferred Term to System Organ Class mapping |
manifest |
Deterministic SHA-256 hashing and Study ID generation |
utils |
Title-casing, label helpers, date formatting |
types |
TypeScript type definitions |
The core signal detection engine. All functions are pure (no side effects, no API calls) and can safely run in Web Workers or server-side.
The main entry point. Computes all disproportionality metrics for a single drug-reaction pair.
import { computeDisproportionality } from "@faerscope/opencore";
import type { ContingencyTable } from "@faerscope/opencore";
const table: ContingencyTable = {
a: 150, // drug + reaction
b: 4850, // drug + NOT reaction
c: 3000, // NOT drug + reaction
d: 992000, // NOT drug + NOT reaction
n: 1000000, // total
};
const result = computeDisproportionality("NAUSEA", 150, table);Returns a DisproportionalityScore with:
| Field | Type | Description |
|---|---|---|
prr |
number |
Proportional Reporting Ratio |
prrChi2 |
number |
Chi-squared statistic (1 df) |
prrPValue |
number |
P-value from chi-squared test |
ror |
number |
Reporting Odds Ratio |
rorLower95 |
number |
ROR 95% CI lower bound (Woolf logit) |
rorUpper95 |
number |
ROR 95% CI upper bound |
ic |
number |
Information Component (log2 scale) |
ic025 |
number |
IC lower 2.5% credibility bound |
ic975 |
number |
IC upper 97.5% credibility bound |
isSignal |
boolean |
Evans criteria: PRR >= 2, Chi2 >= 4, a >= 3 |
isStrongSignal |
boolean |
ROR lower CI > 1 AND IC025 > 0 |
Each metric is also available as a standalone function:
import {
computePRR,
computeROR,
computeROR_CI,
computeIC,
computeIC025,
computeIC975,
computeChi2,
chi2PValue1df,
} from "@faerscope/opencore";
const prr = computePRR(table); // Proportional Reporting Ratio
const ror = computeROR(table); // Reporting Odds Ratio
const { lower, upper } = computeROR_CI(table); // 95% CI for ROR
const ic = computeIC(table); // Information Component
const ic025 = computeIC025(table); // IC lower credibility bound
const ic975 = computeIC975(table); // IC upper credibility bound
const chi2 = computeChi2(table); // Chi-squared statistic
const pValue = chi2PValue1df(chi2); // P-value from chi-squaredApply Benjamini-Hochberg FDR correction to control the false discovery rate when testing multiple drug-reaction pairs simultaneously.
import { applyBenjaminiHochberg } from "@faerscope/opencore";
const corrected = applyBenjaminiHochberg(scores);
// Filter to FDR-significant signals
const significant = corrected.filter(s => (s.fdrPValue ?? 1) < 0.05);Algorithm:
- Rank p-values from smallest to largest
- Adjusted p(i) = min(p(i) * m / rank(i), 1)
- Enforce monotonicity by stepping from largest to smallest rank
import {
detectSpikes,
detectChangepoints,
movingAverage,
rollingZScore,
yearOverYear,
} from "@faerscope/opencore";Detect statistical spikes in a trend time series using z-score thresholding. Combines moving average smoothing with rolling z-score computation.
const annotated = detectSpikes(trendData, 12, 2.0);
const spikes = annotated.filter(p => p.isSpike);
console.log(`Found ${spikes.length} reporting spikes`);| Parameter | Type | Default | Description |
|---|---|---|---|
data |
TrendPoint[] |
required | Time-series data |
window |
number |
12 |
Moving average window size |
threshold |
number |
2.0 |
Z-score threshold for spike flag |
CUSUM (Cumulative Sum) changepoint detection. Identifies structural breaks where the mean reporting rate shifts.
const counts = trendData.map(d => d.count);
const changepoints = detectChangepoints(counts);
// Returns indices where structural breaks were detectedSimple moving average over a numeric array. Returns undefined for points before the window is full.
Rolling centered z-score computation. Useful for outlier detection.
Group time-series data by year and align to month indices for year-over-year comparison charts.
const yoy = yearOverYear(trendData);
for (const [year, months] of yoy) {
console.log(`${year}: ${months.length} months of data`);
}Lightweight, environment-agnostic fetch utilities for the openFDA Drug Adverse Event API.
Note: This module does not include rate limiting. The openFDA API allows 40 requests/minute without a key and 240 requests/minute with a key. Your application is responsible for implementing appropriate throttling. See openFDA Authentication.
All fetch functions accept an optional OpenFDAConfig object:
import type { OpenFDAConfig } from "@faerscope/opencore";
const config: OpenFDAConfig = {
baseUrl: "https://my-proxy.example.com/fda", // Optional: route through a proxy
apiKey: "your-openfda-api-key", // Optional: increases rate limit to 240/min
};Fetch the time-series trend of adverse event reports for a drug.
import { fetchTrend } from "@faerscope/opencore";
const trend = await fetchTrend({
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
serious: true, // optional: restrict to serious reports
});
// trend[0] = { time: "20200102", count: 42, label: "2020-01-02" }Fetch the most frequently reported adverse reactions for a drug.
import { fetchTopReactions } from "@faerscope/opencore";
const reactions = await fetchTopReactions(
{ drug: "metformin", dateFrom: "20200101", dateTo: "20231231" },
20, // top 20 reactions
);
// reactions[0] = { term: "NAUSEA", count: 1234 }Get the total number of reports matching a search query.
import { fetchTotalReports } from "@faerscope/opencore";
const total = await fetchTotalReports({
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
});Build an openFDA search query string from parameters. Useful if you need to construct custom queries.
import { buildSearchString } from "@faerscope/opencore";
const search = buildSearchString({
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
serious: true,
});
// '(patient.drug.openfda.generic_name:"METFORMIN"+patient.drug.openfda.brand_name:"METFORMIN")+AND+receivedate:[20200101+TO+20231231]+AND+serious:1'import {
getDatePreset,
formatDate,
formatDateForInput,
parseDateInput,
} from "@faerscope/opencore";
getDatePreset("3y"); // { from: "20230224", to: "20260224" }
getDatePreset("all"); // { from: "20040101", to: "20260224" }
formatDate(new Date()); // "20260224"
formatDateForInput("20240101"); // "2024-01-01"
parseDateInput("2024-01-01"); // "20240101"Maps MedDRA Preferred Terms to their primary System Organ Class. Covers the ~250 most commonly reported PTs in FAERS (>90% of reactions in typical queries).
Limitation: This is a best-effort mapping based on publicly available MedDRA documentation, not the official MedDRA hierarchy. Some PTs map to multiple SOCs; we assign the primary SOC.
Look up the SOC for a Preferred Term.
import { getSoc } from "@faerscope/opencore";
getSoc("NAUSEA"); // "Gastrointestinal disorders"
getSoc("HEADACHE"); // "Nervous system disorders"
getSoc("RASH"); // "Skin and subcutaneous tissue disorders"
getSoc("UNKNOWN TERM XYZ"); // "Uncategorized"The raw mapping object (uppercase PT keys to SOC strings).
import { PT_TO_SOC } from "@faerscope/opencore";
console.log(PT_TO_SOC["NAUSEA"]); // "Gastrointestinal disorders"Sorted array of all unique SOC names in the mapping.
import { ALL_SOCS } from "@faerscope/opencore";
console.log(ALL_SOCS);
// ["Blood and lymphatic system disorders", "Cardiac disorders", ...]Deterministic SHA-256 hashing and Study ID generation for reproducible analyses. Enables any researcher to verify they are examining the same analysis by comparing Study IDs.
Generate a deterministic Study ID from analysis parameters. The same parameters always produce the same ID.
import { generateStudyId } from "@faerscope/opencore";
const { studyId, fullHash } = await generateStudyId({
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
serious: false,
dedupMode: "filtered",
charMode: "suspect",
});
console.log(studyId); // "FS-a1b2c3d4"
console.log(fullHash); // "a1b2c3d4e5f6..." (64-char hex)Generate a complete reproducibility manifest with parameters, dataset metadata, results summary, and compliance disclaimers.
import { generateManifest } from "@faerscope/opencore";
const manifest = await generateManifest({
params: searchParams,
signals: computedScores,
totalReports: 5000,
fdrEnabled: true,
});
// manifest.studyId, manifest.disclaimer, manifest.limitations, etc.Compute SHA-256 hash using the Web Crypto API. Works in browsers, Node.js 18+, Deno, and Cloudflare Workers.
import { sha256 } from "@faerscope/opencore";
const hash = await sha256("hello world");
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"Build a deterministic JSON string from analysis parameters (excludes API keys and timestamps).
Convert ALL-CAPS MedDRA terms to title case for display.
import { titleCaseTerm } from "@faerscope/opencore";
titleCaseTerm("GASTROOESOPHAGEAL REFLUX DISEASE");
// "Gastrooesophageal Reflux Disease"Get human-readable labels for configuration values.
import { dedupLabel, charLabel } from "@faerscope/opencore";
dedupLabel("filtered"); // "Duplicate-flagged removed"
charLabel("suspect"); // "Suspect role only"Format a YYYYMMDD date range as a readable string.
import { formatDateRange } from "@faerscope/opencore";
formatDateRange("20200101", "20231231"); // "2020-01-01 to 2023-12-31"All types are exported from the package root:
import type {
// Search & configuration
DedupMode,
CharacterizationMode,
SearchParams,
StudioSearchParams,
// Data structures
TrendPoint,
TrendWithStats,
ReactionCount,
ProductCount,
// Disproportionality
ContingencyTable,
DisproportionalityScore,
// Manifest
SessionManifest,
ComparisonConfig,
ManifestInput,
// openFDA
OpenFDAConfig,
OpenFDAMeta,
OpenFDACountResult,
OpenFDAResponse,
} from "@faerscope/opencore";Here's how you might use this library to build a full analysis:
import {
fetchTopReactions,
fetchTotalReports,
buildSearchString,
computeDisproportionality,
applyBenjaminiHochberg,
getSoc,
titleCaseTerm,
generateManifest,
} from "@faerscope/opencore";
import type { ContingencyTable, StudioSearchParams } from "@faerscope/opencore";
const params: StudioSearchParams = {
drug: "metformin",
dateFrom: "20200101",
dateTo: "20231231",
serious: false,
dedupMode: "filtered",
charMode: "suspect",
};
// Step 1: Get total reports for the drug and globally
const drugTotal = await fetchTotalReports(params);
const globalN = 1000000; // You'd fetch this from openFDA too
// Step 2: Get top reactions
const reactions = await fetchTopReactions(params, 50);
// Step 3: For each reaction, build a contingency table and compute scores
// (In practice, you'd fetch 'a' and 'a+c' from openFDA for each reaction)
const scores = reactions.map(r => {
const table: ContingencyTable = {
a: r.count,
b: drugTotal - r.count,
c: 500, // reaction total across all drugs (fetched from openFDA)
d: globalN - drugTotal - 500 + r.count,
n: globalN,
};
return computeDisproportionality(r.term, r.count, table);
});
// Step 4: Apply FDR correction
const corrected = applyBenjaminiHochberg(scores);
// Step 5: Filter to significant signals
const signals = corrected.filter(s => s.isSignal);
const strongSignals = corrected.filter(s => s.isStrongSignal);
// Step 6: Group by organ class
const bySOC = new Map<string, typeof signals>();
for (const s of signals) {
const soc = getSoc(s.reaction);
if (!bySOC.has(soc)) bySOC.set(soc, []);
bySOC.get(soc)!.push(s);
}
// Step 7: Generate reproducibility manifest
const manifest = await generateManifest({
params,
signals: corrected,
totalReports: drugTotal,
fdrEnabled: true,
});
console.log(`Study ID: ${manifest.studyId}`);
console.log(`${signals.length} signals detected (${strongSignals.length} strong)`);| Metric | Formula | Signal Threshold | Reference |
|---|---|---|---|
| PRR | (a/(a+b)) / (c/(c+d)) | >= 2 (Evans) | Evans et al., 2001 |
| ROR | (ad) / (bc) | Lower 95% CI > 1 | Rothman et al., 2004 |
| IC | log2(observed / expected) | IC025 > 0 | Bate et al., 1998 |
| Chi-squared | (ad-bc)^2*N / ((a+b)(c+d)(a+c)(b+d)) | >= 4 (Evans) | Standard 2x2 test |
A drug-reaction pair is classified as a signal when all three conditions are met:
- PRR >= 2
- Chi-squared >= 4
- Case count (cell
a) >= 3
A signal is classified as strong when both of these are met:
- ROR lower 95% CI > 1 (frequentist evidence)
- IC025 > 0 (Bayesian evidence)
When testing many reactions simultaneously, the BH procedure controls the expected proportion of false discoveries among rejected hypotheses. An FDR threshold of 0.05 means you expect no more than 5% of your "signals" to be false positives.
This library and the underlying FAERS data have important limitations:
- FAERS data are voluntary spontaneous reports subject to under-reporting, stimulated reporting, and duplicate submissions.
- Deduplication at the API level is approximate — true case-level dedup requires raw data access.
- Disproportionality metrics assume independence between drug-reaction pairs, which may not hold.
- Small cell counts (<5) produce unstable estimates with wide confidence intervals.
- openFDA aggregation endpoints return approximate counts that may differ from raw FAERS data.
- No denominator data (prescriptions dispensed) is available — cannot compute incidence rates.
- Reporter qualification and country of origin introduce heterogeneity in report quality.
- MedDRA coding variations may split or merge related reactions.
- Temporal trends may reflect changes in reporting behavior rather than true safety signals.
- This is screening-level analysis — any finding requires formal pharmacovigilance review.
| Environment | Supported | Notes |
|---|---|---|
| Node.js 18+ | Yes | Uses crypto.subtle (available since Node 15, stable in 18+) |
| Modern Browsers | Yes | All modern browsers support Web Crypto and Fetch |
| Deno | Yes | Native Web Crypto and Fetch support |
| Cloudflare Workers | Yes | Web Crypto and Fetch available |
| Bun | Yes | Full Web API compatibility |
- FAERScope Studio — Full research workbench with advanced features (NMF, E-values, CUSUM, co-medication networks, auth, session persistence).
- openFDA — The FDA's open API providing access to FAERS and other datasets.
- MedDRA — Medical Dictionary for Regulatory Activities.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Key areas where help is needed:
- Expanding the SOC mapping beyond the current ~250 PTs
- Adding unit tests for edge cases in disproportionality calculations
- Performance benchmarks across different runtimes
- Documentation improvements and tutorials
Apache-2.0 -- Copyright 2024-2026 FAERScope Contributors