GI-BLEED-NSAID: Upper Gastrointestinal Bleeding Risk Stratification Before or During NSAID Therapy in Rheumatic and Autoimmune Disease
GI-BLEED-NSAID: Upper Gastrointestinal Bleeding Risk Stratification Before or During NSAID Therapy in Rheumatic and Autoimmune Disease
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
Abstract
Upper gastrointestinal (UGI) bleeding remains one of the most clinically important preventable harms associated with non-steroidal anti-inflammatory drugs (NSAIDs), especially in rheumatology where older age, glucocorticoids, aspirin, anticoagulants, chronic kidney disease, and prior ulcer history frequently coexist. Clinicians often know that risk is elevated, but bedside decisions still vary on when to add proton pump inhibitor (PPI) protection, prefer a COX-2 selective agent, eradicate Helicobacter pylori, or avoid nonselective NSAIDs altogether. We present GI-BLEED-NSAID, a transparent 10-domain weighted clinical score that estimates near-term UGI bleeding risk before or during NSAID therapy in rheumatic and autoimmune disease. The implementation is executable as standalone Python using only the standard library and includes Monte Carlo uncertainty estimation to reflect modest variability in measured inputs. Three demo scenarios show clinically coherent stratification: younger low-risk naproxen use (LOW), older rheumatoid arthritis with prior ulcer plus aspirin/steroids despite PPI protection (INTERMEDIATE), and prior complicated ulcer bleeding with anticoagulation, H. pylori, steroids, and multiple NSAIDs (VERY HIGH). This score is intended for gastroprotection planning, medication review, and risk communication. It does not diagnose active bleeding, replace endoscopy, or substitute for individualized gastroenterology judgment.
Keywords: NSAID, gastrointestinal bleeding, peptic ulcer, gastroprotection, rheumatology, clinical decision support, DeSci
1. Clinical problem
NSAIDs remain common in rheumatology because they provide rapid analgesic and anti-inflammatory benefit. The tradeoff is well known: mucosal injury, peptic ulcer disease, and upper-GI bleeding. For some patients the added risk is small and manageable. For others—particularly those with prior ulcer complications, aspirin or anticoagulant exposure, older age, systemic steroids, or untreated H. pylori—the danger becomes clinically significant.
The bedside problem is practical rather than theoretical: should the clinician proceed with a nonselective NSAID, add a PPI, switch to a COX-2 selective strategy, eradicate H. pylori, or avoid NSAIDs altogether? GI-BLEED-NSAID was designed to make that decision process transparent and auditable.
2. Methodology
2.1 Design principles
The score was designed around five clinical principles:
- Prior ulcer complications matter most, because recurrence risk strongly shapes future harm.
- Medication stacking matters, especially anticoagulants, aspirin, systemic steroids, and multiple NSAIDs.
- Exposure intensity matters, since high-dose and overlapping NSAID use increase mucosal injury.
- Modifiable protection matters, including PPI use, COX-2 selection, and H. pylori treatment planning.
- Transparency matters, so every domain produces a visible weighted contribution.
2.2 Domains and weights
| Domain | Weight | Rationale |
|---|---|---|
| Age | 0.10 | Bleeding risk rises with age and physiologic vulnerability |
| Ulcer / bleed history | 0.22 | Prior events are one of the strongest recurrence predictors |
| NSAID intensity | 0.14 | High-dose or multiple-NSAID exposure materially increases harm |
| Aspirin | 0.08 | Adds clinically relevant antiplatelet bleeding risk |
| Anticoagulant | 0.12 | Major amplifier of clinically important bleeding |
| Systemic steroids | 0.08 | Common rheumatology co-exposure with GI toxicity implications |
| SSRI | 0.04 | Smaller but recognized bleeding signal |
| H. pylori | 0.08 | Important modifiable ulcer risk factor |
| CKD | 0.06 | Increases medication safety complexity and adverse outcomes |
| Protection | 0.08 | PPI use and COX-2 selection reduce but do not eliminate risk |
Each domain contributes a raw score multiplied by its weight. The final composite score is clamped to 0-100.
2.3 Uncertainty estimation
To avoid false precision, the implementation performs 5,000 Monte Carlo simulations with small perturbations to age and NSAID intensity. The resulting distribution is summarized as an approximate 95% interval.
3. Executable skill
3.1 Python code
#!/usr/bin/env python3
from pathlib import Path
print(Path('skills/gi-bleed-nsaid/gi_bleed_nsaid.py').read_text())The full executable implementation is included in the submission content and locally at skills/gi-bleed-nsaid/gi_bleed_nsaid.py.
3.2 Demo output
=== Scenario 1 — Younger RA patient using short-course standard-dose naproxen ===
Composite score: 1.1/100
Risk category: LOW
=== Scenario 2 — Older RA patient on prednisone and aspirin starting prolonged high-dose NSAID ===
Composite score: 24.4/100
Risk category: INTERMEDIATE
=== Scenario 3 — Prior ulcer bleed, anticoagulation, H. pylori, and multiple NSAIDs ===
Composite score: 50.7/100
Risk category: VERY HIGH4. Interpretation
- LOW (<12): standard NSAID use is usually acceptable if clinically necessary.
- INTERMEDIATE (12-24.9): consider gastroprotection and the lowest effective NSAID dose.
- HIGH (25-39.9): strongly favor a PPI and reconsider nonselective NSAIDs.
- VERY HIGH (≥40): avoid nonselective NSAIDs when feasible and address modifiable risks urgently.
5. Clinical limitations
- The weights are evidence-informed and guideline-aligned but not regression-derived from a prospective cohort.
- The score estimates relative urgency and clinical concern, not absolute event probability.
- Lower-GI toxicity and cardiovascular toxicity are outside the primary scope.
- The model does not capture all endoscopic, frailty, or hepatic risk modifiers.
- Protective effects from PPIs and COX-2 agents are modeled conservatively and do not guarantee safety.
- The score should not delay evaluation of suspected active bleeding.
6. Why this solves a real problem
In routine rheumatology practice, NSAIDs are often started quickly while medication lists and GI history are only partially reviewed. A transparent score can make hidden risk visible, standardize gastroprotection conversations, and help clinicians document why they chose a nonselective NSAID, a COX-2 selective strategy, or a non-NSAID alternative.
References
- Lanza FL, Chan FKL, Quigley EMM; Practice Parameters Committee of the American College of Gastroenterology. Guidelines for prevention of NSAID-related ulcer complications. Am J Gastroenterol. 2009;104(3):728-738. DOI: 10.1038/ajg.2009.115
- Scarpignato C, Lanas A, Blandizzi C, et al. Safe prescribing of non-steroidal anti-inflammatory drugs in patients with osteoarthritis—an expert consensus addressing benefits as well as gastrointestinal and cardiovascular risks. BMC Med. 2015;13:55. DOI: 10.1186/s12916-015-0285-8
- Lanas A, Chan FKL. Peptic ulcer disease. Lancet. 2017;390(10094):613-624. DOI: 10.1016/S0140-6736(16)32404-7
Full Executable Python Code
#!/usr/bin/env python3
"""
GI-BLEED-NSAID: Upper Gastrointestinal Bleeding Risk Stratification Before
or During NSAID Therapy in Rheumatic and Autoimmune Disease
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
License: MIT
Clinical purpose:
Estimate near-term upper gastrointestinal (UGI) bleeding risk when NSAID
therapy is being considered or continued in patients with rheumatic and
autoimmune disease. The score supports gastroprotection planning,
NSAID-selection strategy, and risk communication. It does NOT diagnose active
bleeding, replace endoscopy, or supersede urgent specialist care.
Key references:
- Lanza FL, Chan FKL, Quigley EMM. Am J Gastroenterol. 2009.
DOI: 10.1038/ajg.2009.115
- Scarpignato C et al. BMC Med. 2015.
DOI: 10.1186/s12916-015-0285-8
- Lanas A, Chan FKL. Lancet. 2017.
DOI: 10.1016/S0140-6736(16)32404-7
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class NsaidGiPatient:
age: int = 50
prior_ulcer_or_bleed: bool = False
prior_complicated_bleed: bool = False
high_dose_nsaid: bool = False
multiple_nsaids: bool = False
aspirin: bool = False
anticoagulant: bool = False
systemic_steroids: bool = False
ssri: bool = False
helicobacter_pylori_positive: bool = False
chronic_kidney_disease: bool = False
ppi_gastroprotection: bool = False
cox2_selective_nsaid: bool = False
@dataclass
class DomainScore:
name: str
score: float
weight: float
weighted: float
detail: str
@dataclass
class GiBleedResult:
composite_score: float
risk_category: str
recommendation: str
monitoring_comment: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
WEIGHTS = {
"age": 0.10,
"ulcer_history": 0.22,
"nsaid_intensity": 0.14,
"aspirin": 0.08,
"anticoagulant": 0.12,
"steroids": 0.08,
"ssri": 0.04,
"h_pylori": 0.08,
"ckd": 0.06,
"protection": 0.08,
}
def score_age(age: int):
if age < 60:
return 0, f"Age {age}"
if age < 70:
return 22, f"Age {age}"
if age < 80:
return 40, f"Age {age}"
return 58, f"Age {age}"
def score_ulcer_history(prior_ulcer_or_bleed: bool, prior_complicated_bleed: bool):
if prior_complicated_bleed:
return 92, "Prior complicated ulcer bleeding"
if prior_ulcer_or_bleed:
return 62, "Prior peptic ulcer or non-complicated GI bleed"
return 0, "No prior ulcer or GI bleed history"
def score_nsaid_intensity(high_dose: bool, multiple_nsaids: bool, cox2_selective: bool):
base = 12 if high_dose else 0
if multiple_nsaids:
base += 55
elif high_dose:
base += 18
else:
base += 8
if cox2_selective:
base -= 18
detail = "COX-2 selective NSAID with lower GI toxicity than traditional nonselective NSAIDs"
else:
detail = "Nonselective NSAID exposure"
return max(0, min(base, 80)), detail + ("; high-dose use" if high_dose else "; standard-dose use") + ("; multiple NSAIDs" if multiple_nsaids else "")
def score_binary(flag: bool, if_true: int, yes: str, no: str):
return (if_true, yes) if flag else (0, no)
def score_protection(ppi: bool, cox2_selective: bool):
offset = 0
labels = []
if ppi:
offset -= 42
labels.append("PPI gastroprotection present")
if cox2_selective:
offset -= 18
labels.append("COX-2 selective NSAID lowers ulcer risk")
if not labels:
return 0, "No gastroprotective offset"
return offset, "; ".join(labels)
def compute_gi_bleed_risk(patient: NsaidGiPatient, n_simulations: int = 5000, seed: int = 42) -> GiBleedResult:
items = [
("age", score_age(patient.age)),
("ulcer_history", score_ulcer_history(patient.prior_ulcer_or_bleed, patient.prior_complicated_bleed)),
("nsaid_intensity", score_nsaid_intensity(patient.high_dose_nsaid, patient.multiple_nsaids, patient.cox2_selective_nsaid)),
("aspirin", score_binary(patient.aspirin, 40, "Concomitant aspirin", "No aspirin")),
("anticoagulant", score_binary(patient.anticoagulant, 72, "Concomitant anticoagulant", "No anticoagulant")),
("steroids", score_binary(patient.systemic_steroids, 36, "Concomitant systemic steroids", "No systemic steroids")),
("ssri", score_binary(patient.ssri, 20, "Concomitant SSRI", "No SSRI")),
("h_pylori", score_binary(patient.helicobacter_pylori_positive, 38, "Active or untreated H. pylori", "No known H. pylori")),
("ckd", score_binary(patient.chronic_kidney_disease, 28, "Chronic kidney disease present", "No chronic kidney disease")),
("protection", score_protection(patient.ppi_gastroprotection, patient.cox2_selective_nsaid)),
]
domains: List[DomainScore] = []
composite = 0.0
for name, (raw, detail) in items:
weight = WEIGHTS[name]
weighted = raw * weight
composite += weighted
domains.append(DomainScore(name, round(raw, 1), weight, round(weighted, 1), detail))
composite = round(max(0.0, min(composite, 100.0)), 1)
rng = random.Random(seed)
sims: List[float] = []
for _ in range(n_simulations):
noisy = NsaidGiPatient(
age=max(18, int(round(patient.age + rng.gauss(0, 1.5)))),
prior_ulcer_or_bleed=patient.prior_ulcer_or_bleed,
prior_complicated_bleed=patient.prior_complicated_bleed,
high_dose_nsaid=patient.high_dose_nsaid if rng.random() > 0.03 else not patient.high_dose_nsaid,
multiple_nsaids=patient.multiple_nsaids if rng.random() > 0.02 else not patient.multiple_nsaids,
aspirin=patient.aspirin,
anticoagulant=patient.anticoagulant,
systemic_steroids=patient.systemic_steroids,
ssri=patient.ssri,
helicobacter_pylori_positive=patient.helicobacter_pylori_positive,
chronic_kidney_disease=patient.chronic_kidney_disease,
ppi_gastroprotection=patient.ppi_gastroprotection,
cox2_selective_nsaid=patient.cox2_selective_nsaid,
)
noisy_items = [
("age", score_age(noisy.age)),
("ulcer_history", score_ulcer_history(noisy.prior_ulcer_or_bleed, noisy.prior_complicated_bleed)),
("nsaid_intensity", score_nsaid_intensity(noisy.high_dose_nsaid, noisy.multiple_nsaids, noisy.cox2_selective_nsaid)),
("aspirin", score_binary(noisy.aspirin, 40, "", "")),
("anticoagulant", score_binary(noisy.anticoagulant, 72, "", "")),
("steroids", score_binary(noisy.systemic_steroids, 36, "", "")),
("ssri", score_binary(noisy.ssri, 20, "", "")),
("h_pylori", score_binary(noisy.helicobacter_pylori_positive, 38, "", "")),
("ckd", score_binary(noisy.chronic_kidney_disease, 28, "", "")),
("protection", score_protection(noisy.ppi_gastroprotection, noisy.cox2_selective_nsaid)),
]
total = sum(score * WEIGHTS[name] for name, (score, _) in noisy_items)
sims.append(max(0.0, min(total, 100.0)))
sims.sort()
ci_lower = round(sims[int(0.025 * n_simulations)], 1)
ci_upper = round(sims[int(0.975 * n_simulations)], 1)
if composite < 12:
category = "LOW"
recommendation = "Standard NSAID use is usually acceptable if clinically necessary; avoid unnecessary dose escalation and reassess if new risk factors emerge."
monitoring_comment = "Routine counseling on melena, hematemesis, and dyspepsia; no special GI strategy needed if exposure remains brief."
elif composite < 25:
category = "INTERMEDIATE"
recommendation = "Prefer the lowest effective dose and consider a proton pump inhibitor, especially if treatment will be prolonged."
monitoring_comment = "Review concurrent aspirin, steroid, and SSRI exposure; reassess if age or treatment intensity increases."
elif composite < 40:
category = "HIGH"
recommendation = "Strongly favor gastroprotection and consider a COX-2 selective NSAID or non-NSAID alternative when feasible."
monitoring_comment = "Lower threshold to stop NSAIDs and evaluate urgently if upper-GI symptoms, anemia, or occult blood loss appear."
else:
category = "VERY HIGH"
recommendation = "Avoid nonselective NSAIDs when possible. If an NSAID is unavoidable, use maximal gastroprotection, treat H. pylori, and document the risk-benefit discussion."
monitoring_comment = "This profile justifies early review of alternatives, careful medication reconciliation, and a low threshold for gastroenterology input."
notes = [
"This is a transparent bedside risk-stratification score, not an absolute bleeding probability model.",
"The protection domain reduces but does not eliminate risk because bleeding still occurs despite PPI use or COX-2 selection.",
]
if patient.prior_complicated_bleed:
notes.append("Prior complicated ulcer bleeding is weighted heavily because recurrence risk is clinically substantial.")
if patient.anticoagulant:
notes.append("Concurrent anticoagulation materially raises concern for clinically important upper-GI bleeding.")
return GiBleedResult(
composite_score=composite,
risk_category=category,
recommendation=recommendation,
monitoring_comment=monitoring_comment,
ci_lower=ci_lower,
ci_upper=ci_upper,
domains=[d.__dict__ for d in domains],
notes=notes,
)
def print_case(label: str, patient: NsaidGiPatient):
result = compute_gi_bleed_risk(patient)
print(f"\n=== {label} ===")
print(f"Composite score: {result.composite_score}/100")
print(f"Risk category: {result.risk_category}")
print(f"95% CI: [{result.ci_lower}, {result.ci_upper}]")
print(f"Recommendation: {result.recommendation}")
print(f"Monitoring: {result.monitoring_comment}")
if result.notes:
print("Notes:")
for note in result.notes:
print(f"- {note}")
print("Top domains:")
for domain in sorted(result.domains, key=lambda d: d['weighted'], reverse=True)[:5]:
print(f"- {domain['name']}: raw {domain['score']} × w {domain['weight']} = {domain['weighted']} ({domain['detail']})")
if __name__ == "__main__":
print_case(
"Scenario 1 — Younger RA patient using short-course standard-dose naproxen",
NsaidGiPatient(
age=44,
prior_ulcer_or_bleed=False,
prior_complicated_bleed=False,
high_dose_nsaid=False,
multiple_nsaids=False,
aspirin=False,
anticoagulant=False,
systemic_steroids=False,
ssri=False,
helicobacter_pylori_positive=False,
chronic_kidney_disease=False,
ppi_gastroprotection=False,
cox2_selective_nsaid=False,
),
)
print_case(
"Scenario 2 — Older RA patient on prednisone and aspirin starting prolonged high-dose NSAID",
NsaidGiPatient(
age=68,
prior_ulcer_or_bleed=True,
prior_complicated_bleed=False,
high_dose_nsaid=True,
multiple_nsaids=False,
aspirin=True,
anticoagulant=False,
systemic_steroids=True,
ssri=False,
helicobacter_pylori_positive=False,
chronic_kidney_disease=True,
ppi_gastroprotection=True,
cox2_selective_nsaid=False,
),
)
print_case(
"Scenario 3 — Prior ulcer bleed, anticoagulation, H. pylori, and multiple NSAIDs",
NsaidGiPatient(
age=77,
prior_ulcer_or_bleed=True,
prior_complicated_bleed=True,
high_dose_nsaid=True,
multiple_nsaids=True,
aspirin=False,
anticoagulant=True,
systemic_steroids=True,
ssri=True,
helicobacter_pylori_positive=True,
chronic_kidney_disease=True,
ppi_gastroprotection=False,
cox2_selective_nsaid=False,
),
)
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.