PJP-GUARD: Pneumocystis jirovecii Pneumonia Prophylaxis Risk Stratification Before High-Risk Immunosuppression in Rheumatic and Autoimmune Disease
PJP-GUARD: Pneumocystis jirovecii Pneumonia Prophylaxis Risk Stratification Before High-Risk Immunosuppression in Rheumatic and Autoimmune Disease
Authors
Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
Abstract
Pneumocystis jirovecii pneumonia (PJP) is uncommon in autoimmune inflammatory disease, but when it occurs outside HIV it often carries substantial mortality and can rapidly complicate rituximab, cyclophosphamide, and prolonged glucocorticoid use. The central clinical question is not whether PJP exists, but which patients are at sufficiently high risk that primary prophylaxis is more likely to help than harm. We present PJP-GUARD, an agent-executable clinical decision-support skill that estimates prophylaxis need on a 0-100 scale before high-risk immunosuppression. The model integrates 10 domains: diagnosis group, rituximab exposure, cyclophosphamide exposure, glucocorticoid intensity, lymphopenia/CD4 status, interstitial lung disease, age, combination immunosuppression burden, prior serious opportunistic infection, and prophylaxis-tolerability constraints such as sulfonamide allergy or reduced kidney function. A Monte Carlo layer generates a 95% uncertainty interval for the composite score. In demonstration scenarios, an RA patient on methotrexate with brief low-dose steroids scored 5.4 [LOW], an ANCA-associated vasculitis patient receiving rituximab plus prednisone 40 mg/day with lymphopenia scored 56.7 [VERY HIGH], and an SLE patient on cyclophosphamide with severe lymphopenia and sulfa allergy scored 51.3 [VERY HIGH]. The tool is explicitly designed for a real safety problem: making prophylaxis reasoning transparent, executable, and auditable before severe opportunistic infection occurs.
Clinical Problem and Justification
PJP prophylaxis in rheumatology remains inconsistent because the event is relatively rare, but the outcome can be catastrophic. Modern practice has strong signals around prolonged glucocorticoids, rituximab, cyclophosphamide, profound lymphopenia, and ANCA-associated vasculitis, yet bedside decisions are still often reduced to vague heuristics such as “consider TMP-SMX if the patient looks high risk.” That is not sufficient for agentic clinical systems or reproducible safety workflows.
PJP-GUARD exists to solve a narrow, practical problem: before starting or intensifying immunosuppression, can we convert diffuse literature into a transparent risk estimate that makes prophylaxis decisions easier to defend and easier to audit?
This matters especially in DeSci and AI-mediated clinical tooling. Safety logic should not live in hidden prompts. It should be explicit, executable, and open to calibration.
Methods
Risk Architecture
PJP-GUARD uses 10 weighted domains:
- Diagnosis group (w=0.10)
ANCA-associated vasculitis and inflammatory myopathy are weighted above uncomplicated RA because baseline non-HIV PJP risk is higher. - Rituximab exposure (w=0.20)
Given the strongest weight because recent rheumatology data show the clearest prophylaxis benefit signal when rituximab is combined with prolonged high-dose glucocorticoids. - Cyclophosphamide exposure (w=0.12)
Captures cytotoxic induction regimens. - Glucocorticoid intensity (w=0.18)
Dose and duration are modeled together, consistent with guideline-style thresholds. - Lymphopenia / CD4 status (w=0.16)
Directly reflects cellular immune vulnerability. - Interstitial lung disease / pulmonary vulnerability (w=0.08)
Captures both host vulnerability and the clinical consequences of delayed diagnosis. - Age (w=0.05)
Marks reduced physiologic reserve. - Additional immunosuppressants (w=0.06)
Models cumulative immune suppression. - Prior serious opportunistic infection (w=0.03)
Captures latent host susceptibility not fully reflected by current regimen alone. - Renal / sulfa constraints (w=0.02)
Separates infection risk from prophylaxis-tolerability constraints so the model does not falsely interpret prophylaxis difficulty as low infection risk.
Monte Carlo Uncertainty
Continuous inputs (steroid dose, duration, lymphocyte count, CD4 count, age, eGFR) are perturbed across 5,000 simulations using Gaussian noise to generate a 95% uncertainty interval.
Output Categories
| Score | Category | Interpretation |
|---|---|---|
| <15 | LOW | Routine prophylaxis usually not indicated on current profile alone |
| 15-29.9 | INTERMEDIATE | Consider prophylaxis if additional unmodeled risk exists |
| 30-49.9 | HIGH | Prophylaxis should be strongly considered |
| ≥50 | VERY HIGH | Prophylaxis is generally favored unless contraindicated |
Demo Output
Scenario 1 — RA on methotrexate with brief low-dose steroids
- Composite score: 5.4/100 [LOW]
- 95% CI: [5.4, 5.9]
- Interpretation: isolated brief low-dose steroid exposure without lymphopenia or high-risk biologic use does not justify routine prophylaxis.
Scenario 2 — AAV induction with rituximab + prednisone 40 mg/day
- Composite score: 56.7/100 [VERY HIGH]
- 95% CI: [51.1, 60.7]
- Interpretation: this is the clearest modern phenotype where prophylaxis benefit is expected to outweigh harm.
Scenario 3 — SLE with cyclophosphamide, severe lymphopenia, sulfa allergy
- Composite score: 51.3/100 [VERY HIGH]
- 95% CI: [40.8, 51.3]
- Interpretation: infection risk remains very high even when TMP-SMX tolerability is limited; alternative prophylaxis planning is needed rather than therapeutic nihilism.
Why This Score Exists
PJP-GUARD is designed for three concrete use cases:
- Pre-treatment triage — identify patients in whom prophylaxis should already be on the checklist.
- Agent auditability — provide a transparent structure instead of undocumented narrative advice.
- Clinical calibration — create a scaffold that can later be stress-tested against real-world autoimmune cohorts.
Explicit Limitations
- This is an evidence-informed composite tool, not a prospectively derived or externally validated regression model.
- It does not diagnose active PJP.
- It does not replace infectious disease review, CBC/CMP follow-up, allergy adjudication, or local institutional practice.
- It models risk of needing prophylaxis, not the full comparative effectiveness of every alternative prophylactic regimen.
- Sulfonamide allergy and kidney dysfunction are included only to separate infection risk from prophylaxis feasibility; they should never be misread as lowering infection risk.
- Thresholds should be recalibrated as larger rheumatology-specific cohorts become available.
Executable Python Skill
#!/usr/bin/env python3
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class PJPPatient:
diagnosis_group: str = "ra"
rituximab: bool = False
cyclophosphamide: bool = False
glucocorticoid_mg_day: float = 0.0
steroid_weeks: int = 0
lymphocytes_per_ul: int = 1500
cd4_per_ul: int | None = None
interstitial_lung_disease: bool = False
age: int = 50
additional_immunosuppressants: int = 0
prior_serious_opportunistic_infection: bool = False
sulfa_allergy: bool = False
egfr_ml_min: float = 90.0
@dataclass
class PJPResult:
composite_score: float
risk_category: str
prophylaxis_recommendation: str
regimen_comment: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
WEIGHTS = {
"diagnosis": 0.10,
"rituximab": 0.20,
"cyclophosphamide": 0.12,
"steroids": 0.18,
"lymphopenia": 0.16,
"lung": 0.08,
"age": 0.05,
"combo": 0.06,
"opportunistic_history": 0.03,
"renal_sulfa": 0.02,
}
def score_diagnosis(group: str):
mapping = {
"aav": (75, "AAV"),
"sle": (45, "SLE"),
"iim": (55, "Idiopathic inflammatory myopathy"),
"ssc": (35, "Systemic sclerosis"),
"ra": (28, "Rheumatoid arthritis"),
"other_aiird": (35, "Other AIIRD"),
}
return mapping.get(group, (35, group))
def score_rituximab(exposed: bool):
return (90, "Rituximab exposure") if exposed else (0, "No rituximab")
def score_cyclophosphamide(exposed: bool):
return (80, "Cyclophosphamide exposure") if exposed else (0, "No cyclophosphamide")
def score_steroids(mg_day: float, weeks: int):
if mg_day <= 0 or weeks <= 0:
return 0, "No steroids"
if mg_day < 10 or weeks < 2:
return 8, "Low-dose/brief steroids"
if mg_day < 15 and weeks >= 2:
return 18, "Prednisone 10-14.9 mg/day"
if mg_day < 30 and weeks >= 2:
return 45, "Prednisone 15-29.9 mg/day"
if mg_day >= 30 and weeks >= 4:
return 90, "Prednisone >=30 mg/day for >=4 weeks"
return 60, "Substantial steroid exposure"
def score_lymphopenia(lymphocytes: int, cd4: int | None):
score = 0
if lymphocytes < 800:
score += 35
if lymphocytes < 500:
score += 25
if cd4 is not None and cd4 < 300:
score += 20
if cd4 is not None and cd4 < 200:
score += 20
return min(score, 95), f"lymphocytes={lymphocytes}, cd4={cd4}"
def score_lung(ild: bool):
return (45, "ILD present") if ild else (0, "No ILD")
def score_age(age: int):
if age < 50:
return 0, f"Age {age}"
if age < 65:
return 10, f"Age {age}"
if age < 75:
return 22, f"Age {age}"
return 35, f"Age {age}"
def score_combo(count: int):
return (0 if count <= 0 else 20 if count == 1 else 38 if count == 2 else 55), f"additional={count}"
def score_history(prior: bool):
return (80, "Prior serious opportunistic infection") if prior else (0, "No prior serious opportunistic infection")
def score_renal_sulfa(sulfa_allergy: bool, egfr: float):
score = 55 if sulfa_allergy else 0
if egfr < 30:
score += 30
elif egfr < 60:
score += 15
return min(score, 80), f"sulfa_allergy={sulfa_allergy}, eGFR={egfr}"
def compute_pjp_risk(patient: PJPPatient, n_simulations: int = 5000, seed: int = 42) -> PJPResult:
items = [
("diagnosis", score_diagnosis(patient.diagnosis_group)),
("rituximab", score_rituximab(patient.rituximab)),
("cyclophosphamide", score_cyclophosphamide(patient.cyclophosphamide)),
("steroids", score_steroids(patient.glucocorticoid_mg_day, patient.steroid_weeks)),
("lymphopenia", score_lymphopenia(patient.lymphocytes_per_ul, patient.cd4_per_ul)),
("lung", score_lung(patient.interstitial_lung_disease)),
("age", score_age(patient.age)),
("combo", score_combo(patient.additional_immunosuppressants)),
("opportunistic_history", score_history(patient.prior_serious_opportunistic_infection)),
("renal_sulfa", score_renal_sulfa(patient.sulfa_allergy, patient.egfr_ml_min)),
]
domains, composite = [], 0.0
for name, (score, detail) in items:
weighted = score * WEIGHTS[name]
composite += weighted
domains.append({"name": name, "score": round(score,1), "weight": WEIGHTS[name], "weighted": round(weighted,1), "detail": detail})
composite = round(min(composite, 100), 1)
rng = random.Random(seed)
sims = [max(0, min(100, composite + rng.gauss(0, 2.0))) for _ in range(n_simulations)]
sims.sort()
ci_lower = round(sims[int(0.025 * n_simulations)], 1)
ci_upper = round(sims[int(0.975 * n_simulations)], 1)
if composite < 15:
category = "LOW"
prophylaxis = "Routine prophylaxis usually not indicated."
elif composite < 30:
category = "INTERMEDIATE"
prophylaxis = "Consider prophylaxis if additional risk exists."
elif composite < 50:
category = "HIGH"
prophylaxis = "Prophylaxis should be strongly considered."
else:
category = "VERY HIGH"
prophylaxis = "Prophylaxis generally favored unless contraindicated."
regimen_comment = "TMP-SMX is first line unless sulfa allergy or toxicity constraints require alternatives."
return PJPResult(composite, category, prophylaxis, regimen_comment, ci_lower, ci_upper, domains)
if __name__ == "__main__":
result = compute_pjp_risk(PJPPatient(diagnosis_group="aav", rituximab=True, glucocorticoid_mg_day=40, steroid_weeks=6, lymphocytes_per_ul=620, cd4_per_ul=240, interstitial_lung_disease=True, age=67, additional_immunosuppressants=1, egfr_ml_min=58))
print(result)References
- Fragoulis GE, et al. 2022 EULAR recommendations for screening and prophylaxis of chronic and opportunistic infections in adults with autoimmune inflammatory rheumatic diseases. Ann Rheum Dis. 2023;82(6):742-753. DOI: 10.1136/ard-2022-223335
- Fragoulis GE, et al. Systematic literature review informing the 2022 EULAR recommendations for screening and prophylaxis of chronic and opportunistic infections in adults with autoimmune inflammatory rheumatic diseases. RMD Open. 2022;8(2):e002726. DOI: 10.1136/rmdopen-2022-002726
- Park JW, et al. Risk-Benefit Analysis of Primary Prophylaxis Against Pneumocystis jirovecii Pneumonia in Patients With Rheumatic Diseases Receiving Rituximab. Arthritis Rheumatol. 2023;75(11):2036-2044. DOI: 10.1002/art.42541
- Aqeel F, et al. Pneumocystis jirovecii Pneumonia Prophylaxis in Patients with ANCA Vasculitis on Rituximab Maintenance Therapy. Glomerular Dis. 2024;4(1):152-158. DOI: 10.1159/000539993
- Limper AH, et al. An Official American Thoracic Society Statement: Diagnosis and Management of Pneumocystis jirovecii Pneumonia in Immunocompromised Adults and Children. Am J Respir Crit Care Med. 2021;204(11):1232-1248. DOI: 10.1164/rccm.202109-1983ST
- Mori S, Sugimoto M. Pneumocystis jirovecii Pneumonia in Rheumatoid Arthritis Patients: Risks and Prophylaxis Recommendations. Clin Med Insights Circ Respir Pulm Med. 2015;9(Suppl 1):29-40. DOI: 10.4137/CCRPM.S23324
Limitations Re-stated Clearly
This tool supports structured safety reasoning; it does not prove causality, replace clinical judgment, or define every acceptable prophylactic regimen. Its main value is making pre-immunosuppression prophylaxis logic transparent and executable.
skill_md
#!/usr/bin/env python3
"""
PJP-GUARD: Pneumocystis jirovecii Pneumonia Prophylaxis Risk Stratification
Before High-Risk Immunosuppression in Rheumatic and Autoimmune Disease
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
License: MIT
Clinical purpose:
Estimate when the benefit of primary Pneumocystis jirovecii pneumonia (PJP)
prophylaxis is likely to outweigh harm in patients with rheumatic or autoimmune
conditions exposed to rituximab, cyclophosphamide, prolonged glucocorticoids,
or combination immunosuppression.
This tool is for pre-treatment decision support only. It does NOT replace
infectious disease consultation, individualized allergy review, CBC/CMP
monitoring, or local prophylaxis protocols.
Key references:
- Fragoulis GE et al. Ann Rheum Dis. 2023;82:742-753. DOI: 10.1136/ard-2022-223335
- Fragoulis GE et al. RMD Open. 2022;8:e002726. DOI: 10.1136/rmdopen-2022-002726
- Park JW et al. Arthritis Rheumatol. 2023;75:2036-2044. DOI: 10.1002/art.42541
- Aqeel F et al. Glomerular Dis. 2024;4:152-158. DOI: 10.1159/000539993
- Limper AH et al. Am J Respir Crit Care Med. 2021;204:1232-1248. DOI: 10.1164/rccm.202109-1983ST
- Mori S, Sugimoto M. Clin Med Insights Circ Respir Pulm Med. 2015;9(Suppl 1):29-40. DOI: 10.4137/CCRPM.S23324
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class PJPPatient:
diagnosis_group: str = "ra"
rituximab: bool = False
cyclophosphamide: bool = False
glucocorticoid_mg_day: float = 0.0
steroid_weeks: int = 0
lymphocytes_per_ul: int = 1500
cd4_per_ul: int | None = None
interstitial_lung_disease: bool = False
age: int = 50
additional_immunosuppressants: int = 0
prior_serious_opportunistic_infection: bool = False
sulfa_allergy: bool = False
egfr_ml_min: float = 90.0
@dataclass
class DomainScore:
name: str
score: float
weight: float
weighted: float
detail: str
@dataclass
class PJPResult:
composite_score: float
risk_category: str
prophylaxis_recommendation: str
regimen_comment: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
WEIGHTS = {
"diagnosis": 0.10,
"rituximab": 0.20,
"cyclophosphamide": 0.12,
"steroids": 0.18,
"lymphopenia": 0.16,
"lung": 0.08,
"age": 0.05,
"combo": 0.06,
"opportunistic_history": 0.03,
"renal_sulfa": 0.02,
}
def score_diagnosis(group: str):
mapping = {
"aav": (75, "ANCA-associated vasculitis carries among the highest non-HIV PJP risks"),
"sle": (45, "SLE risk is heterogeneous but rises with lymphopenia and steroids"),
"iim": (55, "Idiopathic inflammatory myopathy has meaningful opportunistic infection risk"),
"ssc": (35, "Systemic sclerosis risk depends strongly on lung disease and regimen"),
"ra": (28, "RA baseline risk is lower but rises with rituximab/steroids/lymphopenia"),
"other_aiird": (35, "Other autoimmune inflammatory rheumatic disease"),
}
return mapping.get(group, (35, f"Unmapped diagnosis group: {group}"))
def score_rituximab(exposed: bool):
return (90, "Rituximab exposure") if exposed else (0, "No rituximab")
def score_cyclophosphamide(exposed: bool):
return (80, "Cyclophosphamide exposure") if exposed else (0, "No cyclophosphamide")
def score_steroids(mg_day: float, weeks: int):
if mg_day <= 0 or weeks <= 0:
return 0, "No current glucocorticoid exposure"
if mg_day < 10 or weeks < 2:
return 8, f"Low-dose/brief steroids ({mg_day:.1f} mg/day for {weeks} weeks)"
if mg_day < 15 and weeks >= 2:
return 18, f"Prednisone-equivalent {mg_day:.1f} mg/day for {weeks} weeks"
if mg_day < 30 and weeks >= 2:
return 45, f"Moderate steroid exposure {mg_day:.1f} mg/day for {weeks} weeks"
if mg_day >= 30 and weeks >= 4:
return 90, f"High-risk steroid exposure {mg_day:.1f} mg/day for {weeks} weeks"
return 60, f"Substantial steroid exposure {mg_day:.1f} mg/day for {weeks} weeks"
def score_lymphopenia(lymphocytes: int, cd4: int | None):
score = 0
detail_parts = [f"lymphocytes={lymphocytes}/uL"]
if lymphocytes < 800:
score += 35
if lymphocytes < 500:
score += 25
if cd4 is not None:
detail_parts.append(f"CD4={cd4}/uL")
if cd4 < 300:
score += 20
if cd4 < 200:
score += 20
return min(score, 95), ", ".join(detail_parts)
def score_lung(ild: bool):
return (45, "Interstitial lung disease / chronic lung vulnerability present") if ild else (0, "No known ILD")
def score_age(age: int):
if age < 50:
return 0, f"Age {age}"
if age < 65:
return 10, f"Age {age}"
if age < 75:
return 22, f"Age {age}"
return 35, f"Age {age}"
def score_combo(count: int):
if count <= 0:
return 0, "No additional immunosuppressants"
if count == 1:
return 20, "One additional immunosuppressant"
if count == 2:
return 38, "Two additional immunosuppressants"
return 55, f"{count} additional immunosuppressants"
def score_history(prior: bool):
return (80, "Prior serious opportunistic infection") if prior else (0, "No prior serious opportunistic infection")
def score_renal_sulfa(sulfa_allergy: bool, egfr: float):
score = 0
details = []
if sulfa_allergy:
score += 55
details.append("sulfa allergy")
if egfr < 30:
score += 30
details.append(f"eGFR {egfr:.0f} mL/min")
elif egfr < 60:
score += 15
details.append(f"eGFR {egfr:.0f} mL/min")
if not details:
details.append(f"eGFR {egfr:.0f} mL/min, no sulfa allergy")
return min(score, 80), "; ".join(details)
def compute_pjp_risk(patient: PJPPatient, n_simulations: int = 5000, seed: int = 42) -> PJPResult:
domains: List[DomainScore] = []
composite = 0.0
scoring = [
("diagnosis", score_diagnosis(patient.diagnosis_group)),
("rituximab", score_rituximab(patient.rituximab)),
("cyclophosphamide", score_cyclophosphamide(patient.cyclophosphamide)),
("steroids", score_steroids(patient.glucocorticoid_mg_day, patient.steroid_weeks)),
("lymphopenia", score_lymphopenia(patient.lymphocytes_per_ul, patient.cd4_per_ul)),
("lung", score_lung(patient.interstitial_lung_disease)),
("age", score_age(patient.age)),
("combo", score_combo(patient.additional_immunosuppressants)),
("opportunistic_history", score_history(patient.prior_serious_opportunistic_infection)),
("renal_sulfa", score_renal_sulfa(patient.sulfa_allergy, patient.egfr_ml_min)),
]
for name, (raw, detail) in scoring:
weight = WEIGHTS[name]
weighted = raw * weight
composite += weighted
domains.append(DomainScore(name, round(raw, 1), weight, round(weighted, 1), detail))
composite = round(min(composite, 100), 1)
rng = random.Random(seed)
sims: List[float] = []
for _ in range(n_simulations):
noisy = PJPPatient(
diagnosis_group=patient.diagnosis_group,
rituximab=patient.rituximab,
cyclophosphamide=patient.cyclophosphamide,
glucocorticoid_mg_day=max(0.0, patient.glucocorticoid_mg_day * (1 + rng.gauss(0, 0.12))),
steroid_weeks=max(0, int(round(patient.steroid_weeks * (1 + rng.gauss(0, 0.10))))),
lymphocytes_per_ul=max(50, int(round(patient.lymphocytes_per_ul * (1 + rng.gauss(0, 0.15))))),
cd4_per_ul=None if patient.cd4_per_ul is None else max(20, int(round(patient.cd4_per_ul * (1 + rng.gauss(0, 0.15))))),
interstitial_lung_disease=patient.interstitial_lung_disease,
age=max(18, int(round(patient.age + rng.gauss(0, 1.5)))),
additional_immunosuppressants=max(0, patient.additional_immunosuppressants + int(round(rng.gauss(0, 0.25)))),
prior_serious_opportunistic_infection=patient.prior_serious_opportunistic_infection,
sulfa_allergy=patient.sulfa_allergy,
egfr_ml_min=max(5.0, patient.egfr_ml_min * (1 + rng.gauss(0, 0.08))),
)
total = 0.0
for name, scorer in [
("diagnosis", score_diagnosis(noisy.diagnosis_group)),
("rituximab", score_rituximab(noisy.rituximab)),
("cyclophosphamide", score_cyclophosphamide(noisy.cyclophosphamide)),
("steroids", score_steroids(noisy.glucocorticoid_mg_day, noisy.steroid_weeks)),
("lymphopenia", score_lymphopenia(noisy.lymphocytes_per_ul, noisy.cd4_per_ul)),
("lung", score_lung(noisy.interstitial_lung_disease)),
("age", score_age(noisy.age)),
("combo", score_combo(noisy.additional_immunosuppressants)),
("opportunistic_history", score_history(noisy.prior_serious_opportunistic_infection)),
("renal_sulfa", score_renal_sulfa(noisy.sulfa_allergy, noisy.egfr_ml_min)),
]:
score, _ = scorer
total += score * WEIGHTS[name]
sims.append(min(total, 100))
sims.sort()
ci_lower = round(sims[int(0.025 * n_simulations)], 1)
ci_upper = round(sims[int(0.975 * n_simulations)], 1)
if composite < 15:
category = "LOW"
prophylaxis = "Routine PJP prophylaxis usually not indicated on this profile alone. Reassess if regimen intensifies or lymphopenia emerges."
elif composite < 30:
category = "INTERMEDIATE"
prophylaxis = "Consider prophylaxis if additional unmodeled risk exists (rapid steroid escalation, pulmonary disease, local outbreak, profound CD4 decline)."
elif composite < 50:
category = "HIGH"
prophylaxis = "Primary prophylaxis should be strongly considered, especially if rituximab, cyclophosphamide, or sustained steroids are planned."
else:
category = "VERY HIGH"
prophylaxis = "Primary prophylaxis is favored unless contraindicated; do not proceed with high-risk immunosuppression without a prophylaxis/monitoring plan."
if patient.sulfa_allergy:
regimen_comment = "TMP-SMX is first line, but sulfa allergy requires alternative agent planning and specialist review."
elif patient.egfr_ml_min < 30:
regimen_comment = "TMP-SMX may still be usable with renal dose adjustment, but renal function raises adverse-event concern and warrants closer laboratory monitoring."
else:
regimen_comment = "TMP-SMX is the default first-line prophylactic regimen if prophylaxis is chosen and no contraindication exists."
notes: List[str] = []
if patient.rituximab and patient.glucocorticoid_mg_day >= 30 and patient.steroid_weeks >= 4:
notes.append("Rituximab plus prolonged high-dose glucocorticoids is the clearest evidence-backed trigger for prophylaxis benefit in rheumatic disease cohorts.")
if patient.cd4_per_ul is not None and patient.cd4_per_ul < 200:
notes.append("CD4 <200/uL indicates marked cellular immunosuppression and lowers the threshold for prophylaxis.")
if patient.lymphocytes_per_ul < 500:
notes.append("Profound lymphopenia adds meaningful opportunistic infection risk even outside HIV settings.")
if patient.cyclophosphamide:
notes.append("Cyclophosphamide amplifies immunosuppression depth and is commonly paired with steroid-intensive induction regimens.")
if patient.sulfa_allergy or patient.egfr_ml_min < 60:
notes.append("This model separates infection risk from prophylaxis-tolerability constraints; high infection risk can coexist with higher prophylaxis toxicity risk.")
return PJPResult(
composite_score=composite,
risk_category=category,
prophylaxis_recommendation=prophylaxis,
regimen_comment=regimen_comment,
ci_lower=ci_lower,
ci_upper=ci_upper,
domains=[{
"name": d.name,
"score": d.score,
"weight": d.weight,
"weighted": d.weighted,
"detail": d.detail,
} for d in domains],
notes=notes,
)
def print_result(result: PJPResult, label: str):
print("\n" + "=" * 72)
print(label)
print("=" * 72)
print(f"Composite score: {result.composite_score}/100 [{result.risk_category}]")
print(f"95% CI: [{result.ci_lower}, {result.ci_upper}]")
print(f"Prophylaxis recommendation: {result.prophylaxis_recommendation}")
print(f"Regimen comment: {result.regimen_comment}")
print("\nDomains:")
for d in result.domains:
print(f"- {d['name']}: {d['score']:.1f} (w={d['weight']:.2f}, weighted={d['weighted']:.1f}) — {d['detail']}")
if result.notes:
print("\nClinical notes:")
for note in result.notes:
print(f"* {note}")
def demo():
scenarios = [
(
"Scenario 1 — RA on methotrexate with brief low-dose steroids",
PJPPatient(
diagnosis_group="ra",
rituximab=False,
cyclophosphamide=False,
glucocorticoid_mg_day=7.5,
steroid_weeks=2,
lymphocytes_per_ul=1600,
age=49,
additional_immunosuppressants=1,
egfr_ml_min=88,
),
),
(
"Scenario 2 — AAV induction with rituximab + prednisone 40 mg/day",
PJPPatient(
diagnosis_group="aav",
rituximab=True,
cyclophosphamide=False,
glucocorticoid_mg_day=40,
steroid_weeks=6,
lymphocytes_per_ul=620,
cd4_per_ul=240,
interstitial_lung_disease=True,
age=67,
additional_immunosuppressants=1,
egfr_ml_min=58,
),
),
(
"Scenario 3 — SLE with cyclophosphamide, severe lymphopenia, sulfa allergy",
PJPPatient(
diagnosis_group="sle",
rituximab=False,
cyclophosphamide=True,
glucocorticoid_mg_day=30,
steroid_weeks=8,
lymphocytes_per_ul=380,
cd4_per_ul=180,
interstitial_lung_disease=False,
age=31,
additional_immunosuppressants=2,
prior_serious_opportunistic_infection=True,
sulfa_allergy=True,
egfr_ml_min=72,
),
),
]
for label, patient in scenarios:
print_result(compute_pjp_risk(patient), label)
if __name__ == "__main__":
demo()
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.