ROMO-CV: Transparent Cardiovascular Contraindication and Risk-Context Stratification Before or During Romosozumab Treatment
ROMO-CV: Transparent Cardiovascular Contraindication and Risk-Context Stratification Before or During Romosozumab Treatment
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
Clinical problem
Romosozumab offers rapid fracture-risk reduction and is attractive in patients with severe osteoporosis, recent fragility fracture, or failure of antiresorptive therapy. The clinical tension is that this same population is often older and enriched for cardiovascular comorbidity. The bedside question is not whether romosozumab works. It is whether the current cardiovascular context is mild enough for routine use, serious enough to require a deliberate benefit-risk review, or severe enough that initiation should stop because recent myocardial infarction, recent stroke, or active ischemic warning symptoms make the decision unsafe.
What ROMO-CV does
ROMO-CV is a dependency-free, reviewer-runnable clinical heuristic that converts auditable bedside features into a 0-100 cardiovascular concern score before or during romosozumab treatment.
It weighs:
- myocardial infarction within the preceding year
- stroke within the preceding year
- active ischemic chest pain or new neurologic deficit
- established ASCVD beyond 12 months
- symptomatic heart failure
- uncontrolled hypertension
- CKD severity
- diabetes
- smoking
- age
- fracture urgency markers: multiple recent fragility fractures, T-score at or below -3, prior antiresorptive failure or intolerance
- availability of another anabolic option
- whether a cardiology review has already been completed
It returns:
- a numeric concern score
- a concern band
- explicit domain-level reasons
- action-oriented guidance
- a Monte Carlo uncertainty interval
Methodology and justification
ROMO-CV is a transparent heuristic risk-context model, not a calibrated prediction tool. The rationale is practical triage. Current evidence shows strong anti-fracture efficacy, but cardiovascular safety signals remain clinically relevant enough that regulators placed a boxed warning on use after recent myocardial infarction or stroke. That means treatment decisions should not be driven by bone density alone.
The model therefore treats three variables as hard-stop conditions:
- myocardial infarction within the preceding year
- stroke within the preceding year
- active ischemic chest pain or new neurologic deficit suggestive of an event
Below that threshold, the score gives substantial weight to established ASCVD, symptomatic heart failure, uncontrolled hypertension, CKD, diabetes, smoking, and age. Fracture urgency lowers the score modestly because therapeutic necessity matters in very-high-risk skeletal disease, but urgency cannot override recent cardiovascular events or active warning symptoms.
Demo output
Running the included Python file prints three scenarios:
- Severe fracture risk without major cardiovascular red flags -> LOW concern
- Older patient with established ASCVD, CKD, and smoking -> VERY HIGH concern
- Recent myocardial infarction before planned initiation -> CONTRAINDICATED/CRITICAL concern
Why this skill is clinically useful
ROMO-CV addresses a real prescribing gap. Many clinicians recognize the boxed warning but still make anabolic decisions informally. A transparent, auditable score can improve shared decision-making, documentation quality, and consistency when balancing fracture urgency against cardiovascular context in osteoporosis care.
Limitations
- Not externally validated.
- Not an absolute risk model for major adverse cardiovascular events.
- Efficacy evidence is strongest in postmenopausal osteoporosis; generalization beyond that population is limited.
- Does not replace emergency assessment, cardiology evaluation, or formal osteoporosis guideline review.
- The 12-month recent-event threshold reflects current regulatory practice rather than a new derivation from patient-level trial data.
References
- Cosman F, Crittenden DB, Adachi JD, et al. Romosozumab Treatment in Postmenopausal Women with Osteoporosis. N Engl J Med. 2016;375(16):1532-1543. DOI: 10.1056/NEJMoa1607948
- Saag KG, Petersen J, Brandi ML, et al. Romosozumab or Alendronate for Fracture Prevention in Women with Osteoporosis. N Engl J Med. 2017;377(15):1417-1427. DOI: 10.1056/NEJMoa1708322
- Stokar J, Szalat A. Cardiovascular Safety of Romosozumab vs PTH Analogues for Osteoporosis Treatment: A Propensity-Score-Matched Cohort Study. J Clin Endocrinol Metab. 2025;110(3):e861-e867. DOI: 10.1210/clinem/dgae173
- Wang Y, Liang T, Cui Y, et al. Cardiovascular Safety of Romosozumab Compared to Commonly Used Anti-osteoporosis Medications in Postmenopausal Osteoporosis: A Systematic Review and Network Meta-analysis of Randomized Controlled Trials. Drug Saf. 2025;48(1):7-23. DOI: 10.1007/s40264-024-01475-9
skill_md
#!/usr/bin/env python3
"""
ROMO-CV: romosozumab cardiovascular contraindication and risk-context
stratification before or during osteoporosis treatment.
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
License: MIT
Clinical purpose:
Make the boxed-warning cardiovascular context around romosozumab explicit at
the bedside, especially when severe fracture risk creates pressure to use an
anabolic agent despite competing cardiac concerns.
This score is a transparent heuristic, not a validated probability model.
It does not replace cardiology, emergency evaluation, or osteoporosis
guideline review.
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class RomoPatient:
planned_or_current_romosozumab: bool = True
myocardial_infarction_within_12_months: bool = False
stroke_within_12_months: bool = False
active_ischemic_chest_pain_or_new_neurologic_deficit: bool = False
established_ascvd_beyond_12_months: bool = False
symptomatic_heart_failure: bool = False
uncontrolled_hypertension: bool = False
egfr: int = 90
diabetes: bool = False
current_smoker: bool = False
age: int = 68
multiple_recent_fragility_fractures: bool = False
t_score_at_or_below_minus_3: bool = False
failed_or_intolerant_antiresorptive_therapy: bool = False
anabolic_alternative_available: bool = True
cardiology_review_completed: bool = False
@dataclass
class DomainScore:
name: str
score: float
detail: str
@dataclass
class RomoCvResult:
composite_score: float
risk_category: str
recommendation: str
safety_alert: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
def yes_no(flag: bool, yes_score: float, yes_text: str, no_text: str):
return (yes_score, yes_text) if flag else (0.0, no_text)
def score_egfr(egfr: int):
if egfr >= 60:
return 0.0, f"eGFR {egfr} mL/min/1.73m^2"
if egfr >= 45:
return 6.0, f"eGFR {egfr} mL/min/1.73m^2"
if egfr >= 30:
return 10.0, f"eGFR {egfr} mL/min/1.73m^2"
return 14.0, f"eGFR {egfr} mL/min/1.73m^2"
def score_age(age: int):
if age < 60:
return 0.0, f"Age {age} years"
if age < 70:
return 4.0, f"Age {age} years"
if age < 80:
return 8.0, f"Age {age} years"
return 12.0, f"Age {age} years"
def score_fracture_urgency(patient: RomoPatient):
urgency = 0.0
reasons = []
if patient.multiple_recent_fragility_fractures:
urgency -= 10.0
reasons.append("multiple recent fragility fractures")
if patient.t_score_at_or_below_minus_3:
urgency -= 6.0
reasons.append("very low bone density")
if patient.failed_or_intolerant_antiresorptive_therapy:
urgency -= 8.0
reasons.append("failed or intolerant antiresorptive therapy")
if reasons:
return urgency, "Fracture urgency supports anabolic need: " + ", ".join(reasons)
return 0.0, "No major fracture-urgency modifier documented"
def compute_romo_cv(patient: RomoPatient, n_simulations: int = 5000, seed: int = 42) -> RomoCvResult:
hard_stop = (
patient.myocardial_infarction_within_12_months
or patient.stroke_within_12_months
or patient.active_ischemic_chest_pain_or_new_neurologic_deficit
)
items = [
("recent_mi", yes_no(patient.myocardial_infarction_within_12_months, 100.0, "Myocardial infarction within the preceding year", "No myocardial infarction within the preceding year")),
("recent_stroke", yes_no(patient.stroke_within_12_months, 100.0, "Stroke within the preceding year", "No stroke within the preceding year")),
("active_symptoms", yes_no(patient.active_ischemic_chest_pain_or_new_neurologic_deficit, 100.0, "Active ischemic chest pain or new neurologic deficit", "No active ischemic or focal neurologic red flags documented")),
("ascvd", yes_no(patient.established_ascvd_beyond_12_months, 24.0, "Established ASCVD beyond 12 months", "No established ASCVD beyond 12 months documented")),
("hf", yes_no(patient.symptomatic_heart_failure, 22.0, "Symptomatic heart failure present", "No symptomatic heart failure documented")),
("htn", yes_no(patient.uncontrolled_hypertension, 12.0, "Uncontrolled hypertension present", "No uncontrolled hypertension documented")),
("renal", score_egfr(patient.egfr)),
("diabetes", yes_no(patient.diabetes, 8.0, "Diabetes present", "No diabetes documented")),
("smoking", yes_no(patient.current_smoker, 8.0, "Current smoker", "Not a current smoker")),
("age", score_age(patient.age)),
("fracture_urgency", score_fracture_urgency(patient)),
("alternative", yes_no(patient.anabolic_alternative_available, 10.0, "Another anabolic option remains available", "No clearly available anabolic alternative documented")),
("cardiology_review", yes_no(patient.cardiology_review_completed, -8.0, "Cardiology review completed", "No dedicated cardiology review documented")),
]
domains: List[DomainScore] = []
total = 0.0
for name, (score, detail) in items:
if name in {"recent_mi", "recent_stroke", "active_symptoms"} and not hard_stop:
score = 0.0
total += score
domains.append(DomainScore(name=name, score=round(score, 1), detail=detail))
if patient.planned_or_current_romosozumab:
total += 4.0
domains.append(DomainScore(name="exposure_context", score=4.0, detail="Romosozumab is actively planned or ongoing"))
total = round(max(0.0, min(total, 100.0)), 1)
if hard_stop:
total = 100.0
rng = random.Random(seed)
sims: List[float] = []
for _ in range(n_simulations):
noisy = RomoPatient(
planned_or_current_romosozumab=patient.planned_or_current_romosozumab,
myocardial_infarction_within_12_months=patient.myocardial_infarction_within_12_months,
stroke_within_12_months=patient.stroke_within_12_months,
active_ischemic_chest_pain_or_new_neurologic_deficit=patient.active_ischemic_chest_pain_or_new_neurologic_deficit,
established_ascvd_beyond_12_months=patient.established_ascvd_beyond_12_months if rng.random() > 0.01 else not patient.established_ascvd_beyond_12_months,
symptomatic_heart_failure=patient.symptomatic_heart_failure if rng.random() > 0.01 else not patient.symptomatic_heart_failure,
uncontrolled_hypertension=patient.uncontrolled_hypertension if rng.random() > 0.02 else not patient.uncontrolled_hypertension,
egfr=max(10, int(round(patient.egfr + rng.gauss(0, 4)))),
diabetes=patient.diabetes,
current_smoker=patient.current_smoker if rng.random() > 0.01 else not patient.current_smoker,
age=max(40, int(round(patient.age + rng.gauss(0, 2)))),
multiple_recent_fragility_fractures=patient.multiple_recent_fragility_fractures,
t_score_at_or_below_minus_3=patient.t_score_at_or_below_minus_3,
failed_or_intolerant_antiresorptive_therapy=patient.failed_or_intolerant_antiresorptive_therapy,
anabolic_alternative_available=patient.anabolic_alternative_available if rng.random() > 0.02 else not patient.anabolic_alternative_available,
cardiology_review_completed=patient.cardiology_review_completed if rng.random() > 0.03 else not patient.cardiology_review_completed,
)
noisy_result = compute_romo_cv(noisy, n_simulations=0, seed=seed)
sims.append(noisy_result.composite_score)
if sims:
sims.sort()
ci_lower = round(sims[int(0.025 * len(sims))], 1)
ci_upper = round(sims[int(0.975 * len(sims))], 1)
else:
ci_lower = total
ci_upper = total
if hard_stop:
category = "CONTRAINDICATED/CRITICAL"
recommendation = (
"Do not initiate or continue routine romosozumab decision-making until the acute cardiovascular or neurologic context is resolved. "
"Urgent medical assessment and an alternative bone strategy are usually more appropriate."
)
alert = (
"This profile crosses a hard-stop threshold aligned with the current boxed-warning context for recent myocardial infarction, recent stroke, "
"or active symptoms suggesting an event."
)
elif total < 12:
category = "LOW"
recommendation = (
"Romosozumab can usually be considered if fracture risk is high, with routine calcium/vitamin D optimization and standard cardiovascular history review."
)
alert = "Low concern does not prove cardiovascular neutrality; it only indicates no major bedside red flags in this framework."
elif total < 25:
category = "INTERMEDIATE"
recommendation = (
"Review modifiable cardiovascular factors, confirm fracture urgency, and document why romosozumab is preferred over other available options."
)
alert = "Intermediate concern should trigger explicit shared decision-making rather than automatic prescribing."
elif total < 45:
category = "HIGH"
recommendation = (
"Use romosozumab only after a deliberate benefit-risk review, with close attention to ASCVD burden, blood pressure control, and alternative therapy options."
)
alert = "The fracture benefit may still matter, but treatment inertia is unsafe when cardiovascular burden is already substantial."
else:
category = "VERY HIGH"
recommendation = (
"Avoid casual initiation. Reassess whether another osteoporosis strategy is safer, and involve cardiology or internal medicine review when anabolic therapy remains compelling."
)
alert = "This profile does not meet a hard-stop threshold, but the cumulative cardiovascular burden is strong enough to challenge routine romosozumab use."
notes = [
"ROMO-CV is a transparent heuristic and does not estimate absolute MACE probability.",
"Fracture urgency lowers the score modestly because therapeutic necessity matters, but it cannot override a recent myocardial infarction, recent stroke, or active warning symptoms.",
"Evidence for efficacy comes mainly from postmenopausal osteoporosis trials; use outside that population requires extra caution."
]
return RomoCvResult(
composite_score=total,
risk_category=category,
recommendation=recommendation,
safety_alert=alert,
ci_lower=ci_lower,
ci_upper=ci_upper,
domains=[d.__dict__ for d in domains],
notes=notes,
)
def print_case(label: str, patient: RomoPatient):
result = compute_romo_cv(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"Safety alert: {result.safety_alert}")
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["score"], reverse=True)[:6]:
print(f"- {domain['name']}: {domain['score']} ({domain['detail']})")
if __name__ == "__main__":
print_case(
"Scenario 1 - Severe fracture risk, no major cardiovascular red flags",
RomoPatient(
planned_or_current_romosozumab=True,
myocardial_infarction_within_12_months=False,
stroke_within_12_months=False,
active_ischemic_chest_pain_or_new_neurologic_deficit=False,
established_ascvd_beyond_12_months=False,
symptomatic_heart_failure=False,
uncontrolled_hypertension=False,
egfr=78,
diabetes=False,
current_smoker=False,
age=67,
multiple_recent_fragility_fractures=True,
t_score_at_or_below_minus_3=True,
failed_or_intolerant_antiresorptive_therapy=True,
anabolic_alternative_available=False,
cardiology_review_completed=False,
),
)
print_case(
"Scenario 2 - Older patient with established ASCVD, CKD, and smoking",
RomoPatient(
planned_or_current_romosozumab=True,
myocardial_infarction_within_12_months=False,
stroke_within_12_months=False,
active_ischemic_chest_pain_or_new_neurologic_deficit=False,
established_ascvd_beyond_12_months=True,
symptomatic_heart_failure=False,
uncontrolled_hypertension=True,
egfr=38,
diabetes=True,
current_smoker=True,
age=76,
multiple_recent_fragility_fractures=True,
t_score_at_or_below_minus_3=True,
failed_or_intolerant_antiresorptive_therapy=False,
anabolic_alternative_available=True,
cardiology_review_completed=False,
),
)
print_case(
"Scenario 3 - Recent myocardial infarction before planned initiation",
RomoPatient(
planned_or_current_romosozumab=True,
myocardial_infarction_within_12_months=True,
stroke_within_12_months=False,
active_ischemic_chest_pain_or_new_neurologic_deficit=False,
established_ascvd_beyond_12_months=True,
symptomatic_heart_failure=True,
uncontrolled_hypertension=True,
egfr=42,
diabetes=True,
current_smoker=False,
age=74,
multiple_recent_fragility_fractures=True,
t_score_at_or_below_minus_3=True,
failed_or_intolerant_antiresorptive_therapy=True,
anabolic_alternative_available=True,
cardiology_review_completed=True,
),
)
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.