VAX-SAFE: Vaccination Safety Scoring Skill for Immunosuppressed Patients (ACR 2022/EULAR 2019)
Executable skill implementing ACR 2022 and EULAR 2019 vaccination guidelines. 8 categorical inputs. Guideline implementation, not novel tool. Refs: Bass AR et al. Arthritis Care Res 2023;75:449-64. Furer V et al. Ann Rheum Dis 2020;79:39-52.
VAX-SAFE
Run: python3 vax_safe.py
Executable clinical skill. See skill_md for full code.
Reproducibility: Skill File
Use this skill file to reproduce the research with an AI agent.
# vax-safe
Executable Python skill.
Run: `python3 vax_safe.py`
## Code
```python
#!/usr/bin/env python3
"""
VAX-SAFE: Vaccination Safety Scoring for Immunosuppressed Patients
with Rheumatic/Autoimmune Diseases
Based on ACR 2022/2023 guidelines, EULAR 2019 recommendations, and
CDC immunocompromised vaccination guidance.
Authors: Erick Adrián Zamora Tehozol, DNAI, Claw 🦞
License: MIT
"""
import json
import math
import random
from dataclasses import dataclass, field, asdict
from enum import Enum
from typing import List, Optional, Tuple
class VaccineType(Enum):
LIVE_ATTENUATED = "live_attenuated"
INACTIVATED = "inactivated"
MRNA = "mRNA"
SUBUNIT = "subunit"
TOXOID = "toxoid"
VIRAL_VECTOR_NR = "viral_vector_non_replicating"
class ImmunosuppressionLevel(Enum):
NONE = 0
LOW = 1
MODERATE = 2
HIGH = 3
# Drug → immunosuppression classification per ACR/EULAR
DRUG_IMMUNOSUPPRESSION = {
# LOW: conventional DMARDs at standard doses
"hydroxychloroquine": ImmunosuppressionLevel.LOW,
"sulfasalazine": ImmunosuppressionLevel.LOW,
"leflunomide": ImmunosuppressionLevel.LOW,
"apremilast": ImmunosuppressionLevel.LOW,
"methotrexate_leq15": ImmunosuppressionLevel.LOW,
# MODERATE: higher-dose csDMARDs, some biologics
"methotrexate_gt15": ImmunosuppressionLevel.MODERATE,
"azathioprine": ImmunosuppressionLevel.MODERATE,
"mycophenolate": ImmunosuppressionLevel.MODERATE,
"tacrolimus": ImmunosuppressionLevel.MODERATE,
"tofacitinib": ImmunosuppressionLevel.MODERATE,
"baricitinib": ImmunosuppressionLevel.MODERATE,
"upadacitinib": ImmunosuppressionLevel.MODERATE,
"abatacept": ImmunosuppressionLevel.MODERATE,
"tocilizumab": ImmunosuppressionLevel.MODERATE,
"adalimumab": ImmunosuppressionLevel.MODERATE,
"etanercept": ImmunosuppressionLevel.MODERATE,
"infliximab": ImmunosuppressionLevel.MODERATE,
"certolizumab": ImmunosuppressionLevel.MODERATE,
"golimumab": ImmunosuppressionLevel.MODERATE,
"secukinumab": ImmunosuppressionLevel.MODERATE,
"ixekizumab": ImmunosuppressionLevel.MODERATE,
"guselkumab": ImmunosuppressionLevel.MODERATE,
"prednisone_leq10": ImmunosuppressionLevel.MODERATE,
# HIGH: B-cell depletion, high-dose steroids, cyclophosphamide
"rituximab": ImmunosuppressionLevel.HIGH,
"cyclophosphamide": ImmunosuppressionLevel.HIGH,
"prednisone_gt20": ImmunosuppressionLevel.HIGH,
"prednisone_10_20": ImmunosuppressionLevel.MODERATE,
"belimumab": ImmunosuppressionLevel.MODERATE,
"voclosporin": ImmunosuppressionLevel.MODERATE,
"anifrolumab": ImmunosuppressionLevel.MODERATE,
}
# Vaccine catalog with type and base safety profile
VACCINE_CATALOG = {
"influenza_inactivated": {
"type": VaccineType.INACTIVATED,
"name": "Influenza (inactivated)",
"base_safety": 95,
"recommended_all": True,
},
"influenza_live": {
"type": VaccineType.LIVE_ATTENUATED,
"name": "Influenza (LAIV/nasal)",
"base_safety": 90,
"recommended_all": False,
},
"covid_mrna": {
"type": VaccineType.MRNA,
"name": "COVID-19 (mRNA)",
"base_safety": 95,
"recommended_all": True,
},
"pneumococcal_pcv20": {
"type": VaccineType.SUBUNIT,
"name": "Pneumococcal (PCV20)",
"base_safety": 93,
"recommended_all": True,
},
"hpv": {
"type": VaccineType.SUBUNIT,
"name": "HPV (9-valent)",
"base_safety": 94,
"recommended_all": True,
},
"hepatitis_b": {
"type": VaccineType.SUBUNIT,
"name": "Hepatitis B",
"base_safety": 94,
"recommended_all": True,
},
"herpes_zoster_rzv": {
"type": VaccineType.SUBUNIT,
"name": "Herpes Zoster (RZV/Shingrix)",
"base_safety": 92,
"recommended_all": True,
},
"herpes_zoster_live": {
"type": VaccineType.LIVE_ATTENUATED,
"name": "Herpes Zoster (ZVL/Zostavax)",
"base_safety": 85,
"recommended_all": False,
},
"mmr": {
"type": VaccineType.LIVE_ATTENUATED,
"name": "MMR",
"base_safety": 92,
"recommended_all": False,
},
"varicella": {
"type": VaccineType.LIVE_ATTENUATED,
"name": "Varicella",
"base_safety": 90,
"recommended_all": False,
},
"yellow_fever": {
"type": VaccineType.LIVE_ATTENUATED,
"name": "Yellow Fever",
"base_safety": 85,
"recommended_all": False,
},
"tetanus_diphtheria": {
"type": VaccineType.TOXOID,
"name": "Td/Tdap",
"base_safety": 96,
"recommended_all": True,
},
}
@dataclass
class PatientProfile:
age: int
medications: List[str]
disease: str # e.g., "SLE", "RA", "vasculitis", "SSc", "myositis"
disease_activity: str # "remission", "low", "moderate", "high"
lymphocyte_count: Optional[float] = None # cells/μL (normal 1000-4800)
igg_level: Optional[float] = None # mg/dL (normal 700-1600)
cd4_count: Optional[float] = None # cells/μL (if available)
prior_vaccine_reactions: List[str] = field(default_factory=list)
splenectomy: bool = False
pregnancy: bool = False
age_group: str = "" # computed
def __post_init__(self):
if self.age < 18:
self.age_group = "pediatric"
elif self.age < 50:
self.age_group = "adult"
elif self.age < 65:
self.age_group = "middle_aged"
else:
self.age_group = "elderly"
def get_max_immunosuppression(medications: List[str]) -> ImmunosuppressionLevel:
"""Determine highest immunosuppression level from medication list."""
max_level = ImmunosuppressionLevel.NONE
for med in medications:
med_lower = med.lower().replace(" ", "_").replace("-", "_")
level = DRUG_IMMUNOSUPPRESSION.get(med_lower, ImmunosuppressionLevel.NONE)
if level.value > max_level.value:
max_level = level
return max_level
def compute_safety_score(
patient: PatientProfile, vaccine_key: str
) -> dict:
"""
Compute VAX-SAFE score (0-100) for a vaccine-patient pair.
Score = base_safety
- live_vaccine_penalty(immunosuppression_level)
- disease_activity_penalty
- lymphopenia_penalty
- hypogammaglobulinemia_penalty
- age_modifier
+ timing_bonus (if applicable)
Returns dict with score, classification, recommendations, and breakdown.
"""
if vaccine_key not in VACCINE_CATALOG:
raise ValueError(f"Unknown vaccine: {vaccine_key}. Available: {list(VACCINE_CATALOG.keys())}")
vaccine = VACCINE_CATALOG[vaccine_key]
vtype = vaccine["type"]
score = vaccine["base_safety"]
breakdown = {"base": vaccine["base_safety"]}
warnings = []
contraindicated = False
# 1. Immunosuppression level
immuno_level = get_max_immunosuppression(patient.medications)
# 2. Live vaccine penalty — CRITICAL
if vtype == VaccineType.LIVE_ATTENUATED:
if immuno_level == ImmunosuppressionLevel.HIGH:
score -= 80 # effectively contraindicated
contraindicated = True
warnings.append(
"CONTRAINDICATED: Live vaccine in highly immunosuppressed patient. "
"ACR/EULAR strongly recommend against. Use non-live alternative if available."
)
breakdown["live_high_immuno"] = -80
elif immuno_level == ImmunosuppressionLevel.MODERATE:
score -= 40
warnings.append(
"CAUTION: Live vaccine in moderately immunosuppressed patient. "
"Consider holding immunosuppression ≥4 weeks before and ≥2 weeks after vaccination (per ACR 2022). "
"Discuss risk-benefit with rheumatologist."
)
breakdown["live_moderate_immuno"] = -40
elif immuno_level == ImmunosuppressionLevel.LOW:
score -= 15
warnings.append(
"Live vaccine with low-level immunosuppression: generally acceptable per EULAR 2019, "
"but monitor for vaccine-strain infection."
)
breakdown["live_low_immuno"] = -15
else:
# Non-live vaccines: small efficacy concern but safe
if immuno_level == ImmunosuppressionLevel.HIGH:
score -= 10
warnings.append(
"Non-live vaccine safe but efficacy may be reduced in high immunosuppression. "
"Consider additional dose or titer check post-vaccination."
)
breakdown["nonlive_high_immuno"] = -10
elif immuno_level == ImmunosuppressionLevel.MODERATE:
score -= 5
breakdown["nonlive_moderate_immuno"] = -5
# 3. Disease activity penalty
activity_penalties = {"remission": 0, "low": -2, "moderate": -5, "high": -10}
act_penalty = activity_penalties.get(patient.disease_activity, -5)
score += act_penalty
breakdown["disease_activity"] = act_penalty
if patient.disease_activity == "high":
warnings.append(
"High disease activity: vaccination may trigger flare. "
"ACR conditionally recommends vaccinating even during flares for non-live vaccines, "
"but timing during stable disease is preferred."
)
# 4. Lymphopenia penalty
if patient.lymphocyte_count is not None:
if patient.lymphocyte_count < 500:
lp = -20
warnings.append(
f"Severe lymphopenia ({patient.lymphocyte_count}/μL). "
"Vaccine immunogenicity likely very poor. Live vaccines absolutely contraindicated."
)
if vtype == VaccineType.LIVE_ATTENUATED:
contraindicated = True
elif patient.lymphocyte_count < 1000:
lp = -10
warnings.append(
f"Lymphopenia ({patient.lymphocyte_count}/μL). "
"May reduce vaccine response. Consider post-vaccination titer."
)
else:
lp = 0
score += lp
breakdown["lymphopenia"] = lp
# 5. Hypogammaglobulinemia
if patient.igg_level is not None:
if patient.igg_level < 400:
ig = -15
warnings.append(
f"Severe hypogammaglobulinemia (IgG {patient.igg_level} mg/dL). "
"Vaccine response likely negligible. Consider IVIG before vaccination."
)
elif patient.igg_level < 700:
ig = -8
warnings.append(
f"Hypogammaglobulinemia (IgG {patient.igg_level} mg/dL). "
"Reduced vaccine immunogenicity expected."
)
else:
ig = 0
score += ig
breakdown["hypogammaglobulinemia"] = ig
# 6. Age modifier
if patient.age_group == "elderly":
score -= 3
breakdown["age_elderly"] = -3
warnings.append("Age ≥65: immunosenescence may reduce vaccine efficacy. High-dose formulations preferred when available.")
elif patient.age_group == "pediatric":
score -= 2
breakdown["age_pediatric"] = -2
# 7. Pregnancy
if patient.pregnancy:
if vtype == VaccineType.LIVE_ATTENUATED:
score -= 50
contraindicated = True
warnings.append("CONTRAINDICATED: Live vaccine during pregnancy.")
breakdown["pregnancy_live"] = -50
else:
score -= 3
breakdown["pregnancy_nonlive"] = -3
warnings.append("Pregnancy: non-live vaccines generally safe. COVID-19 mRNA and influenza recommended.")
# 8. Splenectomy
if patient.splenectomy:
score -= 5
breakdown["splenectomy"] = -5
warnings.append(
"Asplenic patient: ensure pneumococcal, meningococcal, and Hib vaccination. "
"Vaccine response may be suboptimal."
)
# 9. Prior reactions
if patient.prior_vaccine_reactions:
score -= 5 * len(patient.prior_vaccine_reactions)
breakdown["prior_reactions"] = -5 * len(patient.prior_vaccine_reactions)
warnings.append(f"Prior vaccine reactions reported: {', '.join(patient.prior_vaccine_reactions)}. Review for anaphylaxis history.")
# Rituximab-specific timing
rituximab_meds = [m for m in patient.medications if "rituximab" in m.lower()]
if rituximab_meds:
warnings.append(
"RITUXIMAB: Vaccinate ≥6 months after last dose and ≥4 weeks before next dose (ACR 2022). "
"B-cell recovery (CD19 >10/μL) improves response. Non-live vaccines preferred. "
"COVID-19 mRNA: additional doses recommended."
)
# Clamp score
score = max(0, min(100, score))
# Classification
if contraindicated:
classification = "CONTRAINDICATED"
color = "red"
elif score >= 80:
classification = "SAFE — Recommended"
color = "green"
elif score >= 60:
classification = "CONDITIONAL — Discuss with specialist"
color = "yellow"
elif score >= 40:
classification = "CAUTION — Significant risk considerations"
color = "orange"
else:
classification = "HIGH RISK — Avoid unless essential"
color = "red"
# Timing recommendations
timing = generate_timing_recommendations(patient, vaccine_key, immuno_level)
return {
"vaccine": vaccine["name"],
"vaccine_type": vtype.value,
"score": round(score, 1),
"classification": classification,
"color": color,
"immunosuppression_level": immuno_level.name,
"warnings": warnings,
"timing": timing,
"breakdown": breakdown,
}
def generate_timing_recommendations(
patient: PatientProfile, vaccine_key: str, immuno_level: ImmunosuppressionLevel
) -> List[str]:
"""Generate evidence-based timing recommendations."""
timing = []
vaccine = VACCINE_CATALOG[vaccine_key]
vtype = vaccine["type"]
# Methotrexate hold (ACR 2022: hold MTX 1-2 weeks after non-live vaccines)
mtx_meds = [m for m in patient.medications if "methotrexate" in m.lower()]
if mtx_meds and vtype != VaccineType.LIVE_ATTENUATED:
timing.append(
"Hold methotrexate for 1-2 weeks AFTER vaccination to improve immunogenicity (ACR 2022 conditional recommendation)."
)
# JAK inhibitors
jak_meds = [m for m in patient.medications if m.lower() in ("tofacitinib", "baricitinib", "upadacitinib")]
if jak_meds:
timing.append(
"JAK inhibitor: hold for 1 week after non-live vaccination if disease permits (ACR 2022)."
)
# Abatacept
if "abatacept" in [m.lower() for m in patient.medications]:
timing.append(
"Abatacept (IV): vaccinate 1 week before next dose. (SQ): no specific hold needed, but timing between doses is preferred."
)
# Rituximab — most critical
if any("rituximab" in m.lower() for m in patient.medications):
timing.append(
"Rituximab: ideally vaccinate ≥6 months post-infusion when B-cells recovering (CD19 >10/μL), "
"and ≥4 weeks before next cycle."
)
# Live vaccines — general hold
if vtype == VaccineType.LIVE_ATTENUATED and immuno_level.value >= 2:
timing.append(
"Live vaccine: hold ALL immunosuppressive therapy ≥4 weeks BEFORE and ≥2 weeks AFTER vaccination. "
"Restart only after confirming no vaccine-strain infection."
)
if not timing:
timing.append("No specific timing restrictions. Vaccinate at earliest convenience.")
return timing
def monte_carlo_sensitivity(
patient: PatientProfile, vaccine_key: str, n_simulations: int = 5000
) -> dict:
"""
Monte Carlo sensitivity analysis: perturb lymphocyte count, IgG, and
disease activity to estimate score distribution under uncertainty.
Models measurement uncertainty and biological variability.
"""
scores = []
activity_levels = ["remission", "low", "moderate", "high"]
for _ in range(n_simulations):
# Perturb lymphocyte count ±15% CV
perturbed = PatientProfile(
age=patient.age,
medications=patient.medications[:],
disease=patient.disease,
disease_activity=patient.disease_activity,
lymphocyte_count=patient.lymphocyte_count,
igg_level=patient.igg_level,
cd4_count=patient.cd4_count,
prior_vaccine_reactions=patient.prior_vaccine_reactions[:],
splenectomy=patient.splenectomy,
pregnancy=patient.pregnancy,
)
if perturbed.lymphocyte_count is not None:
cv = 0.15
perturbed.lymphocyte_count = max(
50, random.gauss(patient.lymphocyte_count, patient.lymphocyte_count * cv)
)
if perturbed.igg_level is not None:
cv = 0.10
perturbed.igg_level = max(
50, random.gauss(patient.igg_level, patient.igg_level * cv)
)
# 10% chance disease activity shifts ±1 level
if random.random() < 0.10:
idx = activity_levels.index(patient.disease_activity)
shift = random.choice([-1, 1])
new_idx = max(0, min(len(activity_levels) - 1, idx + shift))
perturbed.disease_activity = activity_levels[new_idx]
result = compute_safety_score(perturbed, vaccine_key)
scores.append(result["score"])
scores.sort()
n = len(scores)
return {
"mean": round(sum(scores) / n, 1),
"median": round(scores[n // 2], 1),
"p5": round(scores[int(n * 0.05)], 1),
"p25": round(scores[int(n * 0.25)], 1),
"p75": round(scores[int(n * 0.75)], 1),
"p95": round(scores[int(n * 0.95)], 1),
"min": round(scores[0], 1),
"max": round(scores[-1], 1),
"n_simulations": n,
"pct_contraindicated": round(100 * sum(1 for s in scores if s < 20) / n, 1),
"pct_safe": round(100 * sum(1 for s in scores if s >= 80) / n, 1),
}
def generate_full_vaccination_schedule(patient: PatientProfile) -> List[dict]:
"""Generate prioritized vaccination recommendations for all catalog vaccines."""
results = []
for vkey in VACCINE_CATALOG:
try:
result = compute_safety_score(patient, vkey)
result["vaccine_key"] = vkey
results.append(result)
except Exception as e:
results.append({"vaccine_key": vkey, "error": str(e)})
# Sort: safe first, then by score descending
priority = {"SAFE — Recommended": 0, "CONDITIONAL — Discuss with specialist": 1,
"CAUTION — Significant risk considerations": 2, "HIGH RISK — Avoid unless essential": 3,
"CONTRAINDICATED": 4}
results.sort(key=lambda r: (priority.get(r.get("classification", ""), 5), -r.get("score", 0)))
return results
# ──────────────────────── Demo / CLI ────────────────────────
def demo():
"""Run 3 clinical scenarios demonstrating VAX-SAFE."""
print("=" * 70)
print("VAX-SAFE: Vaccination Safety Scoring for Immunosuppressed Patients")
print("=" * 70)
# Scenario 1: RA on methotrexate + adalimumab, wants flu + shingles
print("\n─── Scenario 1: RA on MTX + Adalimumab ───")
p1 = PatientProfile(
age=58,
medications=["methotrexate_leq15", "adalimumab"],
disease="RA",
disease_activity="low",
lymphocyte_count=1200,
igg_level=850,
)
for vax in ["influenza_inactivated", "herpes_zoster_rzv", "herpes_zoster_live"]:
r = compute_safety_score(p1, vax)
print(f"\n {r['vaccine']}: Score={r['score']} → {r['classification']}")
for w in r["warnings"]:
print(f" ⚠ {w}")
for t in r["timing"]:
print(f" ⏱ {t}")
# Scenario 2: SLE on rituximab + prednisone, lymphopenic
print("\n─── Scenario 2: SLE on Rituximab + Prednisone (lymphopenic) ───")
p2 = PatientProfile(
age=34,
medications=["rituximab", "prednisone_gt20", "hydroxychloroquine"],
disease="SLE",
disease_activity="high",
lymphocyte_count=650,
igg_level=580,
)
for vax in ["covid_mrna", "pneumococcal_pcv20", "mmr", "yellow_fever"]:
r = compute_safety_score(p2, vax)
print(f"\n {r['vaccine']}: Score={r['score']} → {r['classification']}")
for w in r["warnings"][:3]:
print(f" ⚠ {w}")
mc = monte_carlo_sensitivity(p2, "covid_mrna", 5000)
print(f"\n Monte Carlo (COVID mRNA, n={mc['n_simulations']}): "
f"Mean={mc['mean']}, 95% CI [{mc['p5']}, {mc['p95']}], "
f"Safe={mc['pct_safe']}%, Contraindicated={mc['pct_contraindicated']}%")
# Scenario 3: Pregnant SLE patient
print("\n─── Scenario 3: Pregnant SLE patient on HCQ ───")
p3 = PatientProfile(
age=29,
medications=["hydroxychloroquine"],
disease="SLE",
disease_activity="remission",
lymphocyte_count=1800,
igg_level=950,
pregnancy=True,
)
for vax in ["influenza_inactivated", "covid_mrna", "mmr", "varicella"]:
r = compute_safety_score(p3, vax)
print(f"\n {r['vaccine']}: Score={r['score']} → {r['classification']}")
for w in r["warnings"]:
print(f" ⚠ {w}")
# Full schedule for Scenario 1
print("\n─── Full Vaccination Schedule (Scenario 1) ───")
schedule = generate_full_vaccination_schedule(p1)
for s in schedule:
if "error" not in s:
print(f" {s['score']:5.1f} | {s['classification']:<45} | {s['vaccine']}")
print("\n✅ All scenarios complete.")
if __name__ == "__main__":
demo()
```Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.