Spaces:
Sleeping
Sleeping
Deploy SheGuard - Maternal Risk Assessment with Mamba3 SSM
Browse files- .gitignore +28 -0
- Dockerfile +26 -0
- README.md +40 -5
- api/__init__.py +0 -0
- api/alert_logic.py +197 -0
- api/extract_report.py +583 -0
- api/main.py +224 -0
- api/schemas.py +66 -0
- dashboard/app.js +332 -0
- dashboard/index.html +113 -0
- dashboard/styles.css +517 -0
- data/maternal_health.csv +1015 -0
- deployment/ngos_guide.md +51 -0
- models/confusion_matrix.png +0 -0
- models/evaluation_report.txt +107 -0
- models/mamaguard_mamba3.pt +3 -0
- models/roc_curves.png +0 -0
- models/scaler.pkl +3 -0
- requirements.txt +14 -0
- src/__init__.py +0 -0
- src/data_pipeline.py +281 -0
- src/evaluate.py +373 -0
- src/explainability.py +95 -0
- src/model.py +171 -0
- src/predict.py +0 -0
- src/train.py +137 -0
.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Personal notes (not for deployment)
|
| 2 |
+
NOTES.md
|
| 3 |
+
post.md
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*.pyo
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
*.egg
|
| 13 |
+
|
| 14 |
+
# Virtual environments
|
| 15 |
+
.venv/
|
| 16 |
+
venv/
|
| 17 |
+
env/
|
| 18 |
+
|
| 19 |
+
# IDE / Editor
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
*.swp
|
| 23 |
+
*.swo
|
| 24 |
+
*~
|
| 25 |
+
|
| 26 |
+
# OS files
|
| 27 |
+
.DS_Store
|
| 28 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Install Tesseract OCR + system deps
|
| 4 |
+
RUN apt-get update && \
|
| 5 |
+
apt-get install -y --no-install-recommends \
|
| 6 |
+
tesseract-ocr \
|
| 7 |
+
libgl1-mesa-glx \
|
| 8 |
+
libglib2.0-0 && \
|
| 9 |
+
rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
# Install Python deps first (layer caching)
|
| 14 |
+
COPY requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy project files
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# Train the model at build time (generates weights)
|
| 21 |
+
RUN python -m src.train
|
| 22 |
+
|
| 23 |
+
# HF Spaces expects port 7860
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,11 +1,46 @@
|
|
| 1 |
---
|
| 2 |
title: SheGuard
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
short_description:
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: SheGuard
|
| 3 |
+
emoji: 🤱
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
short_description: Maternal mortality early warning using Mamba3 SSM AI
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# SheGuard — Maternal Risk Assessment
|
| 12 |
+
|
| 13 |
+
AI-powered maternal mortality early warning system, built with **Mamba3 Sequential State-Space Models** and **WHO clinical safety rules**.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- **Mamba3 SSM model** — Temporal sequence analysis of prenatal vital signs
|
| 18 |
+
- **3-tier alert system** — GREEN / AMBER / RED risk classification
|
| 19 |
+
- **WHO clinical safety net** — Hard rule overrides for obvious danger signs
|
| 20 |
+
- **OCR auto-fill** — Photograph a paper prenatal record card to auto-populate visit data
|
| 21 |
+
- **Resource-aware routing** — Generates transfer orders when clinic lacks blood supply or staff
|
| 22 |
+
- **Explainable AI** — Shows top contributing features for each prediction
|
| 23 |
+
|
| 24 |
+
## How to Use
|
| 25 |
+
|
| 26 |
+
1. Enter patient vitals from prenatal visits (or upload a photo of the record card)
|
| 27 |
+
2. Click **"Assess maternal risk"**
|
| 28 |
+
3. View the risk level, contributing factors, and recommended clinical actions
|
| 29 |
+
|
| 30 |
+
## Architecture
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
Patient vitals → Mamba3 SSM (5-visit sequence) → Risk prediction
|
| 34 |
+
↓
|
| 35 |
+
WHO Clinical Safety Rules
|
| 36 |
+
↓
|
| 37 |
+
GREEN / AMBER / RED alert
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## Tech Stack
|
| 41 |
+
|
| 42 |
+
- **Model:** PyTorch, Mamba3 SSM (Trapezoidal discretization + Complex state + MIMO)
|
| 43 |
+
- **API:** FastAPI + Uvicorn
|
| 44 |
+
- **Frontend:** HTML/CSS/JS dashboard
|
| 45 |
+
- **OCR:** OpenCV + Pytesseract
|
| 46 |
+
- **Dataset:** UCI Maternal Health Risk (1,014 samples)
|
api/__init__.py
ADDED
|
File without changes
|
api/alert_logic.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — Alert Logic
|
| 3 |
+
Three-tier alerting with suppression, clinical safety net, and resource-aware routing.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from typing import Optional, Dict
|
| 8 |
+
from api.schemas import AlertTier
|
| 9 |
+
|
| 10 |
+
# ─── Thresholds ────────────────────────────────────────────────────────────────
|
| 11 |
+
RED_THRESHOLD = 0.85
|
| 12 |
+
AMBER_THRESHOLD = 0.65
|
| 13 |
+
RED_CONSECUTIVE_VISITS = 2
|
| 14 |
+
SUPPRESSION_HOURS = 48
|
| 15 |
+
SUPPRESSION_DELTA = 0.15
|
| 16 |
+
|
| 17 |
+
# In-memory alert store (use Redis/DB in production)
|
| 18 |
+
_alert_history: Dict[str, dict] = {}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ─── Clinical rule safety net ─────────────────────────────────────────────────
|
| 22 |
+
|
| 23 |
+
def apply_clinical_safety_net(visits_raw: list) -> tuple[str | None, str | None]:
|
| 24 |
+
"""
|
| 25 |
+
Checks raw visit values against WHO clinical thresholds.
|
| 26 |
+
Returns (forced_tier, reason) if a rule fires, else (None, None).
|
| 27 |
+
"""
|
| 28 |
+
max_systolic = max((v.get("systolic_bp") or 0) for v in visits_raw)
|
| 29 |
+
max_diastolic = max((v.get("diastolic_bp") or 0) for v in visits_raw)
|
| 30 |
+
max_bs = max((v.get("blood_sugar") or 0) for v in visits_raw)
|
| 31 |
+
|
| 32 |
+
latest = visits_raw[-1]
|
| 33 |
+
latest_sys = latest.get("systolic_bp") or 0
|
| 34 |
+
latest_dia = latest.get("diastolic_bp") or 0
|
| 35 |
+
|
| 36 |
+
# ── RED RULES ──────────────────────────────────────────────────────────────
|
| 37 |
+
|
| 38 |
+
# Rule 1: Severe hypertension → RED
|
| 39 |
+
if max_systolic >= 160 or max_diastolic >= 110:
|
| 40 |
+
return "RED", (
|
| 41 |
+
f"SystolicBP {max_systolic} mmHg meets WHO severe "
|
| 42 |
+
f"hypertension threshold (≥160) — emergency referral required"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Rule 5: Multi-vital simultaneous escalation → RED
|
| 46 |
+
if len(visits_raw) >= 3:
|
| 47 |
+
first = visits_raw[0]
|
| 48 |
+
last = visits_raw[-1]
|
| 49 |
+
|
| 50 |
+
sys_rise = (last.get("systolic_bp") or 0) - (first.get("systolic_bp") or 0)
|
| 51 |
+
dia_rise = (last.get("diastolic_bp") or 0) - (first.get("diastolic_bp") or 0)
|
| 52 |
+
bs_rise = (last.get("blood_sugar") or 0) - (first.get("blood_sugar") or 0)
|
| 53 |
+
hr_rise = (last.get("heart_rate") or 0) - (first.get("heart_rate") or 0)
|
| 54 |
+
temp_rise = (last.get("body_temp") or 0) - (first.get("body_temp") or 0)
|
| 55 |
+
|
| 56 |
+
escalating_count = 0
|
| 57 |
+
if sys_rise >= 15: escalating_count += 1
|
| 58 |
+
if dia_rise >= 10: escalating_count += 1
|
| 59 |
+
if bs_rise >= 3.0: escalating_count += 1
|
| 60 |
+
if hr_rise >= 15: escalating_count += 1
|
| 61 |
+
if temp_rise >= 0.5: escalating_count += 1
|
| 62 |
+
|
| 63 |
+
if escalating_count >= 3:
|
| 64 |
+
return "RED", (
|
| 65 |
+
f"{escalating_count} vitals escalating simultaneously "
|
| 66 |
+
f"(BP +{sys_rise:.0f} mmHg, HR +{hr_rise:.0f} bpm, "
|
| 67 |
+
f"BS +{bs_rise:.1f} mmol/L) — combined deterioration "
|
| 68 |
+
f"pattern, refer immediately"
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# ── AMBER RULES ────────────────────────────────────────────────────────────
|
| 72 |
+
|
| 73 |
+
# Rule 2: Hypertension in pregnancy → AMBER
|
| 74 |
+
if latest_sys >= 140 or latest_dia >= 90:
|
| 75 |
+
return "AMBER", (
|
| 76 |
+
f"SystolicBP {latest_sys} mmHg meets WHO hypertension "
|
| 77 |
+
f"in pregnancy threshold (≥140)"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Rule 3: Severe hyperglycaemia → AMBER
|
| 81 |
+
if max_bs > 11.1:
|
| 82 |
+
return "AMBER", (
|
| 83 |
+
f"Blood sugar {max_bs} mmol/L exceeds gestational "
|
| 84 |
+
f"diabetes threshold (>11.1)"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Rule 4: BP escalation pattern → AMBER
|
| 88 |
+
if len(visits_raw) >= 2:
|
| 89 |
+
first_sys = visits_raw[0].get("systolic_bp") or 0
|
| 90 |
+
bp_rise = latest_sys - first_sys
|
| 91 |
+
if bp_rise >= 20:
|
| 92 |
+
return "AMBER", (
|
| 93 |
+
f"SystolicBP rose {bp_rise:.0f} mmHg across visits "
|
| 94 |
+
f"— escalation pattern detected"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return None, None
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def compute_alert_tier(
|
| 101 |
+
probabilities: dict,
|
| 102 |
+
patient_id: str,
|
| 103 |
+
visit_importances: list,
|
| 104 |
+
) -> tuple[AlertTier, bool]:
|
| 105 |
+
"""
|
| 106 |
+
Computes the alert tier for a patient.
|
| 107 |
+
Returns: (alert_tier, is_suppressed)
|
| 108 |
+
"""
|
| 109 |
+
high_risk_prob = probabilities.get("High risk", 0.0)
|
| 110 |
+
|
| 111 |
+
# Check suppression
|
| 112 |
+
if patient_id in _alert_history:
|
| 113 |
+
prev = _alert_history[patient_id]
|
| 114 |
+
time_since = datetime.now() - prev["last_alerted"]
|
| 115 |
+
score_delta = high_risk_prob - prev["last_score"]
|
| 116 |
+
|
| 117 |
+
if time_since < timedelta(hours=SUPPRESSION_HOURS):
|
| 118 |
+
if score_delta < SUPPRESSION_DELTA:
|
| 119 |
+
return prev["tier"], True
|
| 120 |
+
|
| 121 |
+
# Determine tier
|
| 122 |
+
if high_risk_prob >= RED_THRESHOLD:
|
| 123 |
+
recent_weight = sum(visit_importances[-RED_CONSECUTIVE_VISITS:])
|
| 124 |
+
if recent_weight > 0.4:
|
| 125 |
+
tier = AlertTier.RED
|
| 126 |
+
else:
|
| 127 |
+
tier = AlertTier.AMBER
|
| 128 |
+
elif high_risk_prob >= AMBER_THRESHOLD:
|
| 129 |
+
tier = AlertTier.AMBER
|
| 130 |
+
else:
|
| 131 |
+
tier = AlertTier.GREEN
|
| 132 |
+
|
| 133 |
+
# Store in history
|
| 134 |
+
_alert_history[patient_id] = {
|
| 135 |
+
"last_alerted": datetime.now(),
|
| 136 |
+
"last_score": high_risk_prob,
|
| 137 |
+
"tier": tier,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
return tier, False
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def generate_action_text(
|
| 144 |
+
tier: AlertTier,
|
| 145 |
+
top_reasons: list,
|
| 146 |
+
data_quality: float,
|
| 147 |
+
staff_available: Optional[int],
|
| 148 |
+
blood_units: Optional[int],
|
| 149 |
+
) -> tuple[str, Optional[str]]:
|
| 150 |
+
"""Generates actionable text for the health worker and optional transfer order."""
|
| 151 |
+
|
| 152 |
+
transfer_order = None
|
| 153 |
+
reasons_text = "; ".join(top_reasons) if top_reasons else "multiple elevated indicators"
|
| 154 |
+
|
| 155 |
+
if tier == AlertTier.GREEN:
|
| 156 |
+
action = "Continue standard prenatal care schedule. No immediate action required."
|
| 157 |
+
|
| 158 |
+
elif tier == AlertTier.AMBER:
|
| 159 |
+
action = (
|
| 160 |
+
f"AMBER ALERT: Elevated risk detected ({reasons_text}). "
|
| 161 |
+
f"Schedule additional checkup within 72 hours. "
|
| 162 |
+
f"Increase monitoring frequency to weekly."
|
| 163 |
+
)
|
| 164 |
+
if data_quality < 0.7:
|
| 165 |
+
action += " NOTE: Data quality is low — ensure all readings are recorded at next visit."
|
| 166 |
+
|
| 167 |
+
else: # RED
|
| 168 |
+
action = (
|
| 169 |
+
f"🔴 RED ALERT: High maternal risk detected ({reasons_text}). "
|
| 170 |
+
f"Refer patient to hospital immediately for full assessment."
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
# Resource-aware routing
|
| 174 |
+
resources_ok = True
|
| 175 |
+
resource_warnings = []
|
| 176 |
+
|
| 177 |
+
if staff_available is not None and staff_available == 0:
|
| 178 |
+
resources_ok = False
|
| 179 |
+
resource_warnings.append("no doctors currently on duty")
|
| 180 |
+
if blood_units is not None and blood_units < 2:
|
| 181 |
+
resources_ok = False
|
| 182 |
+
resource_warnings.append("insufficient blood supply (<2 units)")
|
| 183 |
+
|
| 184 |
+
if not resources_ok:
|
| 185 |
+
issues = " and ".join(resource_warnings)
|
| 186 |
+
transfer_order = (
|
| 187 |
+
f"TRANSFER ORDER: This clinic has {issues}. "
|
| 188 |
+
f"Patient must be transferred to nearest equipped facility. "
|
| 189 |
+
f"Call referral line: 1800-MAMA-REF. "
|
| 190 |
+
f"Document: Patient at high risk of {reasons_text}."
|
| 191 |
+
)
|
| 192 |
+
action += f" ⚠️ Resources insufficient — transfer order generated."
|
| 193 |
+
|
| 194 |
+
if data_quality < 0.7:
|
| 195 |
+
action += " NOTE: Prediction confidence is reduced due to missing readings."
|
| 196 |
+
|
| 197 |
+
return action, transfer_order
|
api/extract_report.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# api/extract_report.py
|
| 2 |
+
"""
|
| 3 |
+
MamaGuard — Report Extraction (OCR)
|
| 4 |
+
Ensemble preprocessing × ensemble OCR for extracting prenatal data from images.
|
| 5 |
+
Handles screenshots, paper photos, scans, low-light, rotated, colored forms, etc.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import base64
|
| 9 |
+
import io
|
| 10 |
+
import re
|
| 11 |
+
import pytesseract
|
| 12 |
+
from PIL import Image
|
| 13 |
+
import cv2
|
| 14 |
+
import numpy as np
|
| 15 |
+
from fastapi import APIRouter, HTTPException
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
from typing import Optional, List, Tuple
|
| 18 |
+
|
| 19 |
+
router = APIRouter()
|
| 20 |
+
|
| 21 |
+
# Tesseract path (Windows)
|
| 22 |
+
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ── Schemas ───────────────────────────────────────────────────────────────────
|
| 26 |
+
|
| 27 |
+
class ExtractRequest(BaseModel):
|
| 28 |
+
image_base64: str
|
| 29 |
+
image_type: str = "image/jpeg"
|
| 30 |
+
|
| 31 |
+
class ExtractedVisit(BaseModel):
|
| 32 |
+
age: Optional[float] = None
|
| 33 |
+
systolic_bp: Optional[float] = None
|
| 34 |
+
diastolic_bp: Optional[float] = None
|
| 35 |
+
blood_sugar: Optional[float] = None
|
| 36 |
+
body_temp: Optional[float] = None
|
| 37 |
+
heart_rate: Optional[float] = None
|
| 38 |
+
visit_date: Optional[str] = None
|
| 39 |
+
|
| 40 |
+
class ExtractResponse(BaseModel):
|
| 41 |
+
visits: List[ExtractedVisit]
|
| 42 |
+
patient_id: Optional[str] = None
|
| 43 |
+
notes: Optional[str] = None
|
| 44 |
+
confidence: float = 1.0
|
| 45 |
+
raw_text: Optional[str] = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ── Medical ranges (values outside = OCR errors, discard) ─────────────────────
|
| 49 |
+
RANGES = {
|
| 50 |
+
"age": (10, 60),
|
| 51 |
+
"systolic_bp": (70, 200),
|
| 52 |
+
"diastolic_bp": (40, 130),
|
| 53 |
+
"blood_sugar": (3.0, 20.0),
|
| 54 |
+
"body_temp": (35.0, 42.0),
|
| 55 |
+
"heart_rate": (40, 160),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Field label aliases
|
| 59 |
+
ROW_ALIASES = {
|
| 60 |
+
"age": ["age", "years", "yr", "patient age", "age (yrs)"],
|
| 61 |
+
"systolic_bp": ["systolic", "sbp", "sys", "systolic bp", "bp sys",
|
| 62 |
+
"upper bp", "s.b.p", "s bp", "syst"],
|
| 63 |
+
"diastolic_bp": ["diastolic", "dbp", "dia", "diastolic bp", "bp dia",
|
| 64 |
+
"lower bp", "d.b.p", "d bp", "diast"],
|
| 65 |
+
"blood_sugar": ["blood sugar", "bs", "glucose", "bg", "blood glucose",
|
| 66 |
+
"sugar", "b.s", "rbs", "fbs", "ppbs", "glu"],
|
| 67 |
+
"body_temp": ["temp", "temperature", "body temp", "tmp", "fever",
|
| 68 |
+
"body temperature", "b.temp", "t (c)", "t(c)"],
|
| 69 |
+
"heart_rate": ["heart rate", "hr", "pulse", "bpm", "heartrate",
|
| 70 |
+
"heart", "p/r", "pr", "pulse rate"],
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Lines containing these words are skipped during parsing
|
| 74 |
+
SKIP_KEYWORDS = [
|
| 75 |
+
"field", "visit", "v1", "v2", "v3", "v 1", "v 2", "v 3",
|
| 76 |
+
"column", "header", "parameter", "reading", "measurement",
|
| 77 |
+
"no.", "sl.", "s.no", "item"
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 82 |
+
# IMAGE PREPROCESSING PIPELINES
|
| 83 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 84 |
+
|
| 85 |
+
def to_gray(pil_image: Image.Image) -> np.ndarray:
|
| 86 |
+
"""Convert PIL image to OpenCV grayscale array."""
|
| 87 |
+
return cv2.cvtColor(np.array(pil_image.convert("RGB")), cv2.COLOR_RGB2GRAY)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def smart_upscale(gray: np.ndarray, target_width: int = 1400) -> np.ndarray:
|
| 91 |
+
"""Upscale small images so Tesseract can read text reliably."""
|
| 92 |
+
h, w = gray.shape
|
| 93 |
+
if w < target_width:
|
| 94 |
+
scale = target_width / w
|
| 95 |
+
gray = cv2.resize(gray, None, fx=scale, fy=scale,
|
| 96 |
+
interpolation=cv2.INTER_CUBIC)
|
| 97 |
+
return gray
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def detect_and_invert_if_dark(gray: np.ndarray) -> np.ndarray:
|
| 101 |
+
"""Invert dark-background images for Tesseract (trained on black-on-white)."""
|
| 102 |
+
if np.mean(gray) < 127:
|
| 103 |
+
return cv2.bitwise_not(gray)
|
| 104 |
+
return gray
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def remove_shadow(gray: np.ndarray) -> np.ndarray:
|
| 108 |
+
"""Remove uneven lighting via background subtraction."""
|
| 109 |
+
dilated = cv2.dilate(gray, np.ones((7, 7), np.uint8))
|
| 110 |
+
bg = cv2.medianBlur(dilated, 21)
|
| 111 |
+
diff = 255 - cv2.absdiff(gray, bg)
|
| 112 |
+
norm = cv2.normalize(diff, None, 0, 255, cv2.NORM_MINMAX)
|
| 113 |
+
return norm.astype(np.uint8)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def enhance_contrast_clahe(gray: np.ndarray) -> np.ndarray:
|
| 117 |
+
"""Apply CLAHE for adaptive local contrast enhancement."""
|
| 118 |
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
| 119 |
+
return clahe.apply(gray)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def deskew(gray: np.ndarray) -> np.ndarray:
|
| 123 |
+
"""Correct image rotation using minimum-area bounding rectangle."""
|
| 124 |
+
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
| 125 |
+
coords = np.column_stack(np.where(binary > 0))
|
| 126 |
+
if len(coords) < 100:
|
| 127 |
+
return gray
|
| 128 |
+
angle = cv2.minAreaRect(coords)[-1]
|
| 129 |
+
if angle < -45:
|
| 130 |
+
angle = 90 + angle
|
| 131 |
+
if abs(angle) > 15:
|
| 132 |
+
return gray
|
| 133 |
+
(h, w) = gray.shape
|
| 134 |
+
M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0)
|
| 135 |
+
return cv2.warpAffine(gray, M, (w, h),
|
| 136 |
+
flags=cv2.INTER_CUBIC,
|
| 137 |
+
borderMode=cv2.BORDER_REPLICATE)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def remove_ruled_lines(gray: np.ndarray) -> np.ndarray:
|
| 141 |
+
"""Remove horizontal ruled lines that confuse text segmentation."""
|
| 142 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1))
|
| 143 |
+
lines = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel, iterations=2)
|
| 144 |
+
return cv2.subtract(gray, lines)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ── 8 Preprocessing pipelines ─────────────────────────────────────────────────
|
| 148 |
+
|
| 149 |
+
def pipeline_standard(gray: np.ndarray) -> np.ndarray:
|
| 150 |
+
"""Pipeline A: Clean printed document, white paper, good lighting."""
|
| 151 |
+
gray = smart_upscale(gray)
|
| 152 |
+
gray = detect_and_invert_if_dark(gray)
|
| 153 |
+
gray = cv2.fastNlMeansDenoising(gray, h=10)
|
| 154 |
+
return cv2.adaptiveThreshold(gray, 255,
|
| 155 |
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def pipeline_shadow(gray: np.ndarray) -> np.ndarray:
|
| 159 |
+
"""Pipeline B: Paper photo with shadow or uneven lighting."""
|
| 160 |
+
gray = smart_upscale(gray)
|
| 161 |
+
gray = detect_and_invert_if_dark(gray)
|
| 162 |
+
gray = remove_shadow(gray)
|
| 163 |
+
gray = cv2.fastNlMeansDenoising(gray, h=8)
|
| 164 |
+
return cv2.adaptiveThreshold(gray, 255,
|
| 165 |
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 41, 12)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def pipeline_low_contrast(gray: np.ndarray) -> np.ndarray:
|
| 169 |
+
"""Pipeline C: Faded ink, old paper, poor scan quality."""
|
| 170 |
+
gray = smart_upscale(gray)
|
| 171 |
+
gray = detect_and_invert_if_dark(gray)
|
| 172 |
+
gray = enhance_contrast_clahe(gray)
|
| 173 |
+
gray = cv2.fastNlMeansDenoising(gray, h=12)
|
| 174 |
+
blurred = cv2.GaussianBlur(gray, (0, 0), 3)
|
| 175 |
+
gray = cv2.addWeighted(gray, 1.5, blurred, -0.5, 0)
|
| 176 |
+
return cv2.adaptiveThreshold(gray, 255,
|
| 177 |
+
cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 21, 8)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def pipeline_deskewed(gray: np.ndarray) -> np.ndarray:
|
| 181 |
+
"""Pipeline D: Rotated or skewed image."""
|
| 182 |
+
gray = smart_upscale(gray)
|
| 183 |
+
gray = detect_and_invert_if_dark(gray)
|
| 184 |
+
gray = deskew(gray)
|
| 185 |
+
gray = cv2.fastNlMeansDenoising(gray, h=10)
|
| 186 |
+
return cv2.adaptiveThreshold(gray, 255,
|
| 187 |
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def pipeline_ruled_paper(gray: np.ndarray) -> np.ndarray:
|
| 191 |
+
"""Pipeline E: Handwritten on ruled/lined paper."""
|
| 192 |
+
gray = smart_upscale(gray)
|
| 193 |
+
gray = detect_and_invert_if_dark(gray)
|
| 194 |
+
gray = remove_shadow(gray)
|
| 195 |
+
gray = remove_ruled_lines(gray)
|
| 196 |
+
gray = enhance_contrast_clahe(gray)
|
| 197 |
+
gray = cv2.fastNlMeansDenoising(gray, h=15)
|
| 198 |
+
return cv2.adaptiveThreshold(gray, 255,
|
| 199 |
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 25, 8)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def pipeline_high_noise(gray: np.ndarray) -> np.ndarray:
|
| 203 |
+
"""Pipeline F: Low-light photo, dim room, heavily compressed JPEG."""
|
| 204 |
+
gray = smart_upscale(gray, target_width=1800)
|
| 205 |
+
gray = detect_and_invert_if_dark(gray)
|
| 206 |
+
gray = cv2.fastNlMeansDenoising(gray, h=20,
|
| 207 |
+
templateWindowSize=7, searchWindowSize=21)
|
| 208 |
+
gray = enhance_contrast_clahe(gray)
|
| 209 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
|
| 210 |
+
gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
|
| 211 |
+
_, out = cv2.threshold(gray, 0, 255,
|
| 212 |
+
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 213 |
+
return out
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def pipeline_screenshot(gray: np.ndarray) -> np.ndarray:
|
| 217 |
+
"""Pipeline G: Screenshot (pixel-perfect, minimal preprocessing needed)."""
|
| 218 |
+
gray = smart_upscale(gray)
|
| 219 |
+
gray = detect_and_invert_if_dark(gray)
|
| 220 |
+
_, out = cv2.threshold(gray, 0, 255,
|
| 221 |
+
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 222 |
+
return out
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def pipeline_color_form(pil_image: Image.Image) -> np.ndarray:
|
| 226 |
+
"""Pipeline H: Colored background — uses LAB color space for contrast."""
|
| 227 |
+
img_bgr = cv2.cvtColor(np.array(pil_image.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 228 |
+
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
|
| 229 |
+
l_chan = lab[:, :, 0]
|
| 230 |
+
l_chan = smart_upscale(l_chan)
|
| 231 |
+
l_chan = detect_and_invert_if_dark(l_chan)
|
| 232 |
+
l_chan = enhance_contrast_clahe(l_chan)
|
| 233 |
+
l_chan = cv2.fastNlMeansDenoising(l_chan, h=10)
|
| 234 |
+
return cv2.adaptiveThreshold(l_chan, 255,
|
| 235 |
+
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
PIPELINE_FNS = {
|
| 239 |
+
"standard": pipeline_standard,
|
| 240 |
+
"shadow": pipeline_shadow,
|
| 241 |
+
"low_contrast": pipeline_low_contrast,
|
| 242 |
+
"deskewed": pipeline_deskewed,
|
| 243 |
+
"ruled_paper": pipeline_ruled_paper,
|
| 244 |
+
"high_noise": pipeline_high_noise,
|
| 245 |
+
"screenshot": pipeline_screenshot,
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
PIPELINE_ORDERS = {
|
| 249 |
+
"screenshot": ["screenshot", "standard", "low_contrast"],
|
| 250 |
+
"dark_bg": ["screenshot", "standard", "shadow"],
|
| 251 |
+
"low_contrast": ["low_contrast", "shadow", "standard", "high_noise"],
|
| 252 |
+
"colored_form": ["color_form", "low_contrast", "standard"],
|
| 253 |
+
"camera_photo": ["shadow", "deskewed", "ruled_paper", "standard"],
|
| 254 |
+
"standard": ["standard", "shadow", "low_contrast", "deskewed"],
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 259 |
+
# IMAGE TYPE DETECTOR
|
| 260 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 261 |
+
|
| 262 |
+
def detect_image_type(gray: np.ndarray, pil_image: Image.Image) -> str:
|
| 263 |
+
"""Heuristically classify the image to select optimal pipeline order."""
|
| 264 |
+
mean_b = float(np.mean(gray))
|
| 265 |
+
std_b = float(np.std(gray))
|
| 266 |
+
h, w = gray.shape
|
| 267 |
+
|
| 268 |
+
if std_b > 75 and (mean_b < 70 or mean_b > 185):
|
| 269 |
+
return "screenshot"
|
| 270 |
+
|
| 271 |
+
if mean_b < 85:
|
| 272 |
+
return "dark_bg"
|
| 273 |
+
|
| 274 |
+
if std_b < 35:
|
| 275 |
+
return "low_contrast"
|
| 276 |
+
|
| 277 |
+
img_hsv = cv2.cvtColor(np.array(pil_image.convert("RGB")), cv2.COLOR_RGB2HSV)
|
| 278 |
+
mean_sat = float(np.mean(img_hsv[:, :, 1]))
|
| 279 |
+
if mean_sat > 40:
|
| 280 |
+
return "colored_form"
|
| 281 |
+
|
| 282 |
+
if w > 2000 or h > 2000:
|
| 283 |
+
return "camera_photo"
|
| 284 |
+
|
| 285 |
+
return "standard"
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 289 |
+
# OCR ENGINE
|
| 290 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 291 |
+
|
| 292 |
+
OCR_CONFIGS = [
|
| 293 |
+
"--psm 6 --oem 3",
|
| 294 |
+
"--psm 4 --oem 3",
|
| 295 |
+
"--psm 11 --oem 3",
|
| 296 |
+
"--psm 3 --oem 3",
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def run_ocr_best(processed_img: np.ndarray) -> str:
|
| 301 |
+
"""Run Tesseract with all PSM configs and return the output with most digits."""
|
| 302 |
+
pil = Image.fromarray(processed_img)
|
| 303 |
+
best_text = ""
|
| 304 |
+
best_digits = 0
|
| 305 |
+
for config in OCR_CONFIGS:
|
| 306 |
+
try:
|
| 307 |
+
text = pytesseract.image_to_string(pil, config=config)
|
| 308 |
+
digits = sum(c.isdigit() for c in text)
|
| 309 |
+
if digits > best_digits:
|
| 310 |
+
best_digits = digits
|
| 311 |
+
best_text = text
|
| 312 |
+
except Exception:
|
| 313 |
+
continue
|
| 314 |
+
return best_text
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 318 |
+
# PARSERS
|
| 319 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 320 |
+
|
| 321 |
+
def find_numbers(text: str) -> List[float]:
|
| 322 |
+
"""Extract all integers and decimals from a string."""
|
| 323 |
+
return [float(m) for m in re.findall(r'\d+\.?\d*', text)]
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def match_label(line: str) -> Optional[str]:
|
| 327 |
+
"""Check if a line contains a known field name alias. Returns field key or None."""
|
| 328 |
+
ll = line.lower()
|
| 329 |
+
for field, aliases in ROW_ALIASES.items():
|
| 330 |
+
for alias in aliases:
|
| 331 |
+
if alias in ll:
|
| 332 |
+
return field
|
| 333 |
+
return None
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def is_skip_line(line: str) -> bool:
|
| 337 |
+
"""Returns True if this line is a header or label-only row to skip."""
|
| 338 |
+
ll = line.lower()
|
| 339 |
+
return any(kw in ll for kw in SKIP_KEYWORDS)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def parse_table_format(raw_text: str) -> List[dict]:
|
| 343 |
+
"""Parser A — Row-per-field table layout."""
|
| 344 |
+
lines = [l.strip() for l in raw_text.split('\n') if l.strip()]
|
| 345 |
+
field_vals = {}
|
| 346 |
+
max_visits = 0
|
| 347 |
+
|
| 348 |
+
for line in lines:
|
| 349 |
+
if is_skip_line(line):
|
| 350 |
+
continue
|
| 351 |
+
field = match_label(line)
|
| 352 |
+
if field is None:
|
| 353 |
+
continue
|
| 354 |
+
numbers = find_numbers(line)
|
| 355 |
+
lo, hi = RANGES[field]
|
| 356 |
+
valid = [n for n in numbers if lo <= n <= hi]
|
| 357 |
+
if valid:
|
| 358 |
+
field_vals[field] = valid
|
| 359 |
+
max_visits = max(max_visits, len(valid))
|
| 360 |
+
|
| 361 |
+
if not field_vals:
|
| 362 |
+
return []
|
| 363 |
+
|
| 364 |
+
return [
|
| 365 |
+
{field: (values[i] if i < len(values) else None)
|
| 366 |
+
for field, values in field_vals.items()}
|
| 367 |
+
for i in range(max_visits)
|
| 368 |
+
]
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def parse_column_format(raw_text: str) -> List[dict]:
|
| 372 |
+
"""Parser B — Column-per-visit layout with header row."""
|
| 373 |
+
lines = [l.strip() for l in raw_text.split('\n') if l.strip()]
|
| 374 |
+
header_idx = -1
|
| 375 |
+
header_fields = []
|
| 376 |
+
|
| 377 |
+
for i, line in enumerate(lines):
|
| 378 |
+
parts = re.split(r'\s{2,}|\t', line)
|
| 379 |
+
known = [match_label(p) for p in parts]
|
| 380 |
+
if sum(f is not None for f in known) >= 3:
|
| 381 |
+
header_idx = i
|
| 382 |
+
header_fields = known
|
| 383 |
+
break
|
| 384 |
+
|
| 385 |
+
if header_idx == -1:
|
| 386 |
+
return []
|
| 387 |
+
|
| 388 |
+
visits = []
|
| 389 |
+
for line in lines[header_idx + 1:]:
|
| 390 |
+
if is_skip_line(line):
|
| 391 |
+
continue
|
| 392 |
+
parts = re.split(r'\s{2,}|\t', line)
|
| 393 |
+
visit = {}
|
| 394 |
+
for col_idx, field in enumerate(header_fields):
|
| 395 |
+
if field is None or col_idx >= len(parts):
|
| 396 |
+
continue
|
| 397 |
+
nums = find_numbers(parts[col_idx])
|
| 398 |
+
if nums:
|
| 399 |
+
lo, hi = RANGES[field]
|
| 400 |
+
val = nums[0]
|
| 401 |
+
if lo <= val <= hi:
|
| 402 |
+
visit[field] = val
|
| 403 |
+
if visit:
|
| 404 |
+
visits.append(visit)
|
| 405 |
+
return visits
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def parse_key_value_format(raw_text: str) -> List[dict]:
|
| 409 |
+
"""Parser C — Key-value format (one field per line)."""
|
| 410 |
+
lines = [l.strip() for l in raw_text.split('\n') if l.strip()]
|
| 411 |
+
visit = {}
|
| 412 |
+
|
| 413 |
+
for line in lines:
|
| 414 |
+
if is_skip_line(line):
|
| 415 |
+
continue
|
| 416 |
+
field = match_label(line)
|
| 417 |
+
if field is None:
|
| 418 |
+
continue
|
| 419 |
+
|
| 420 |
+
# Handle BP written as "112/74"
|
| 421 |
+
bp_match = re.search(r'(\d{2,3})\s*/\s*(\d{2,3})', line)
|
| 422 |
+
if bp_match and field in ("systolic_bp", "diastolic_bp"):
|
| 423 |
+
s, d = float(bp_match.group(1)), float(bp_match.group(2))
|
| 424 |
+
if 70 <= s <= 200: visit["systolic_bp"] = s
|
| 425 |
+
if 40 <= d <= 130: visit["diastolic_bp"] = d
|
| 426 |
+
continue
|
| 427 |
+
|
| 428 |
+
nums = find_numbers(line)
|
| 429 |
+
lo, hi = RANGES[field]
|
| 430 |
+
valid = [n for n in nums if lo <= n <= hi]
|
| 431 |
+
if valid:
|
| 432 |
+
visit[field] = valid[0]
|
| 433 |
+
|
| 434 |
+
return [visit] if visit else []
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def score_visits(visits: List[dict]) -> int:
|
| 438 |
+
"""Count total non-None extracted values across all visits."""
|
| 439 |
+
return sum(1 for v in visits for val in v.values() if val is not None)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def best_parse(raw_text: str) -> Tuple[List[dict], str]:
|
| 443 |
+
"""Try all parsers and return the one that extracted the most data."""
|
| 444 |
+
candidates = [
|
| 445 |
+
(parse_table_format(raw_text), "row-per-field"),
|
| 446 |
+
(parse_column_format(raw_text), "column-per-visit"),
|
| 447 |
+
(parse_key_value_format(raw_text),"key-value"),
|
| 448 |
+
]
|
| 449 |
+
return max(candidates, key=lambda x: score_visits(x[0]))
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def extract_patient_id(raw_text: str) -> Optional[str]:
|
| 453 |
+
"""Find common patient ID patterns in OCR text."""
|
| 454 |
+
patterns = [
|
| 455 |
+
r'PT[-\s]?\d{4}[-\s]?\d+',
|
| 456 |
+
r'Patient\s*(ID|No\.?|Number)[:\s]+([\w-]+)',
|
| 457 |
+
r'Reg(?:istration)?\s*(No\.?|#|:)\s*([\w-]+)',
|
| 458 |
+
r'ANC\s*(?:No\.?|#|:)?\s*([\w-]+)',
|
| 459 |
+
r'Card\s*(?:No\.?|#)[:\s]*([\w-]+)',
|
| 460 |
+
r'P/(\d+)',
|
| 461 |
+
r'MRN[:\s]+([\w-]+)',
|
| 462 |
+
]
|
| 463 |
+
for pattern in patterns:
|
| 464 |
+
m = re.search(pattern, raw_text, re.IGNORECASE)
|
| 465 |
+
if m:
|
| 466 |
+
groups = [g for g in m.groups() if g]
|
| 467 |
+
return groups[-1] if groups else m.group(0)
|
| 468 |
+
return None
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 472 |
+
# MAIN ENDPOINT
|
| 473 |
+
# ═══════════════════════════════════════════════════════════════════════════════
|
| 474 |
+
|
| 475 |
+
@router.post("/extract-report", response_model=ExtractResponse)
|
| 476 |
+
async def extract_report(request: ExtractRequest):
|
| 477 |
+
"""
|
| 478 |
+
POST /extract-report — Offline OCR extraction from prenatal report images.
|
| 479 |
+
Runs multiple preprocessing pipelines × OCR configs, returns best result.
|
| 480 |
+
"""
|
| 481 |
+
|
| 482 |
+
# 1. Decode image
|
| 483 |
+
try:
|
| 484 |
+
image_bytes = base64.b64decode(request.image_base64)
|
| 485 |
+
pil_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 486 |
+
except Exception as e:
|
| 487 |
+
raise HTTPException(
|
| 488 |
+
status_code=400,
|
| 489 |
+
detail=f"Could not decode image: {e}"
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# 2. Detect image type → get pipeline order
|
| 493 |
+
gray = to_gray(pil_image)
|
| 494 |
+
img_type = detect_image_type(gray, pil_image)
|
| 495 |
+
pipelines = PIPELINE_ORDERS.get(img_type, PIPELINE_ORDERS["standard"])
|
| 496 |
+
|
| 497 |
+
# 3. Run each pipeline → OCR → parse → score
|
| 498 |
+
best_visits = []
|
| 499 |
+
best_score = 0
|
| 500 |
+
best_text = ""
|
| 501 |
+
best_pipeline = "none"
|
| 502 |
+
best_parser = "none"
|
| 503 |
+
all_texts = []
|
| 504 |
+
|
| 505 |
+
for pipeline_name in pipelines:
|
| 506 |
+
try:
|
| 507 |
+
if pipeline_name == "color_form":
|
| 508 |
+
processed = pipeline_color_form(pil_image)
|
| 509 |
+
else:
|
| 510 |
+
fn = PIPELINE_FNS.get(pipeline_name, pipeline_standard)
|
| 511 |
+
processed = fn(to_gray(pil_image))
|
| 512 |
+
except Exception:
|
| 513 |
+
continue
|
| 514 |
+
|
| 515 |
+
try:
|
| 516 |
+
raw_text = run_ocr_best(processed)
|
| 517 |
+
except Exception:
|
| 518 |
+
continue
|
| 519 |
+
|
| 520 |
+
all_texts.append(raw_text[:200])
|
| 521 |
+
|
| 522 |
+
visits, parser_name = best_parse(raw_text)
|
| 523 |
+
score = score_visits(visits)
|
| 524 |
+
|
| 525 |
+
if score > best_score:
|
| 526 |
+
best_score = score
|
| 527 |
+
best_visits = visits
|
| 528 |
+
best_text = raw_text
|
| 529 |
+
best_pipeline = pipeline_name
|
| 530 |
+
best_parser = parser_name
|
| 531 |
+
|
| 532 |
+
# 4. Handle complete failure
|
| 533 |
+
if not best_visits or best_score == 0:
|
| 534 |
+
fallback_text = all_texts[0][:300] if all_texts else "No text extracted"
|
| 535 |
+
return ExtractResponse(
|
| 536 |
+
visits = [ExtractedVisit()],
|
| 537 |
+
patient_id = None,
|
| 538 |
+
notes = (
|
| 539 |
+
f"Image type detected: {img_type}. "
|
| 540 |
+
"No structured data could be extracted. "
|
| 541 |
+
"Likely causes: very blurry, pure handwriting, or unusual layout. "
|
| 542 |
+
f"Raw OCR text: {fallback_text}"
|
| 543 |
+
),
|
| 544 |
+
confidence = 0.0,
|
| 545 |
+
raw_text = fallback_text,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# 5. Build ExtractedVisit objects
|
| 549 |
+
extracted = [
|
| 550 |
+
ExtractedVisit(
|
| 551 |
+
age = v.get("age"),
|
| 552 |
+
systolic_bp = v.get("systolic_bp"),
|
| 553 |
+
diastolic_bp = v.get("diastolic_bp"),
|
| 554 |
+
blood_sugar = v.get("blood_sugar"),
|
| 555 |
+
body_temp = v.get("body_temp"),
|
| 556 |
+
heart_rate = v.get("heart_rate"),
|
| 557 |
+
)
|
| 558 |
+
for v in best_visits
|
| 559 |
+
]
|
| 560 |
+
|
| 561 |
+
total_possible = len(extracted) * 6
|
| 562 |
+
total_filled = sum(
|
| 563 |
+
1 for v in extracted
|
| 564 |
+
for val in [v.age, v.systolic_bp, v.diastolic_bp,
|
| 565 |
+
v.blood_sugar, v.body_temp, v.heart_rate]
|
| 566 |
+
if val is not None
|
| 567 |
+
)
|
| 568 |
+
confidence = round(total_filled / max(total_possible, 1), 2)
|
| 569 |
+
|
| 570 |
+
notes = (
|
| 571 |
+
f"Image type: {img_type} | "
|
| 572 |
+
f"Pipeline: {best_pipeline} | "
|
| 573 |
+
f"Parser: {best_parser} | "
|
| 574 |
+
f"{total_filled}/{total_possible} fields extracted."
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
return ExtractResponse(
|
| 578 |
+
visits = extracted,
|
| 579 |
+
patient_id = extract_patient_id(best_text),
|
| 580 |
+
notes = notes,
|
| 581 |
+
confidence = confidence,
|
| 582 |
+
raw_text = best_text[:500],
|
| 583 |
+
)
|
api/main.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard -- FastAPI Server
|
| 3 |
+
Maternal risk prediction API with clinical safety net and explainability.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pickle
|
| 9 |
+
import os
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from fastapi.staticfiles import StaticFiles
|
| 13 |
+
from fastapi.responses import FileResponse
|
| 14 |
+
|
| 15 |
+
from api.schemas import PredictionRequest, PredictionResponse, AlertTier
|
| 16 |
+
from api.alert_logic import compute_alert_tier, generate_action_text
|
| 17 |
+
from src.model import MamaGuardMamba3
|
| 18 |
+
from src.explainability import explain_prediction
|
| 19 |
+
from api.extract_report import router as extract_router
|
| 20 |
+
|
| 21 |
+
# --- App setup ----------------------------------------------------------------
|
| 22 |
+
app = FastAPI(
|
| 23 |
+
title="SheGuard -- Maternal Risk API",
|
| 24 |
+
description=(
|
| 25 |
+
"Predicts maternal mortality risk from prenatal visit sequences. "
|
| 26 |
+
"Built with Mamba3 SSM for deployment in low-resource clinics."
|
| 27 |
+
),
|
| 28 |
+
version="1.0.0",
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
app.include_router(extract_router)
|
| 32 |
+
|
| 33 |
+
app.add_middleware(
|
| 34 |
+
CORSMiddleware,
|
| 35 |
+
allow_origins=["*"],
|
| 36 |
+
allow_methods=["*"],
|
| 37 |
+
allow_headers=["*"],
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# ─── Model loading ────────────────────────────────────────────────────────────
|
| 41 |
+
|
| 42 |
+
MODEL_PATH = "models/mamaguard_mamba3.pt"
|
| 43 |
+
SCALER_PATH = "models/scaler.pkl"
|
| 44 |
+
|
| 45 |
+
model = None
|
| 46 |
+
scaler = None
|
| 47 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 48 |
+
|
| 49 |
+
FEATURE_ORDER = ['Age', 'SystolicBP', 'DiastolicBP', 'BS', 'BodyTemp', 'HeartRate']
|
| 50 |
+
FEATURE_DEFAULTS = {
|
| 51 |
+
'Age': 30.0, 'SystolicBP': 120.0, 'DiastolicBP': 80.0,
|
| 52 |
+
'BS': 7.5, 'BodyTemp': 36.8, 'HeartRate': 76.0
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@app.on_event("startup")
|
| 57 |
+
async def load_model():
|
| 58 |
+
"""Load model and scaler at server startup."""
|
| 59 |
+
global model, scaler
|
| 60 |
+
|
| 61 |
+
if not os.path.exists(MODEL_PATH):
|
| 62 |
+
print(f"WARNING: Model not found at {MODEL_PATH}. Run training first.")
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
model = MamaGuardMamba3(input_dim=6, d_model=64, n_layers=4, n_classes=3, d_state=32)
|
| 66 |
+
model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
|
| 67 |
+
model.to(device)
|
| 68 |
+
model.eval()
|
| 69 |
+
|
| 70 |
+
with open(SCALER_PATH, "rb") as f:
|
| 71 |
+
scaler = pickle.load(f)
|
| 72 |
+
|
| 73 |
+
print(f"MamaGuard model loaded on {device}")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# --- Routes -------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
@app.get("/health")
|
| 79 |
+
async def health_check():
|
| 80 |
+
"""Health check endpoint for load balancers."""
|
| 81 |
+
return {
|
| 82 |
+
"status": "healthy",
|
| 83 |
+
"model_loaded": model is not None,
|
| 84 |
+
"device": device,
|
| 85 |
+
"version": "1.0.0",
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@app.post("/predict", response_model=PredictionResponse)
|
| 90 |
+
async def predict(request: PredictionRequest):
|
| 91 |
+
"""
|
| 92 |
+
Process patient visit data and return risk prediction with explanation.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
if model is None or scaler is None:
|
| 96 |
+
raise HTTPException(
|
| 97 |
+
status_code=503,
|
| 98 |
+
detail="Model not loaded. Contact system administrator."
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Prepare visit data
|
| 102 |
+
visits = request.visits
|
| 103 |
+
visit_map = {
|
| 104 |
+
'Age': 'age', 'SystolicBP': 'systolic_bp', 'DiastolicBP': 'diastolic_bp',
|
| 105 |
+
'BS': 'blood_sugar', 'BodyTemp': 'body_temp', 'HeartRate': 'heart_rate'
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
raw_array = []
|
| 109 |
+
missing_counts = []
|
| 110 |
+
|
| 111 |
+
for visit in visits:
|
| 112 |
+
row = []
|
| 113 |
+
missing = 0
|
| 114 |
+
for feat in FEATURE_ORDER:
|
| 115 |
+
attr = visit_map[feat]
|
| 116 |
+
val = getattr(visit, attr, None)
|
| 117 |
+
if val is None:
|
| 118 |
+
val = FEATURE_DEFAULTS[feat]
|
| 119 |
+
missing += 1
|
| 120 |
+
row.append(val)
|
| 121 |
+
raw_array.append(row)
|
| 122 |
+
missing_counts.append(missing)
|
| 123 |
+
|
| 124 |
+
raw_np = np.array(raw_array, dtype=np.float32)
|
| 125 |
+
|
| 126 |
+
# Data quality score
|
| 127 |
+
total_fields = len(visits) * len(FEATURE_ORDER)
|
| 128 |
+
missing_total = sum(missing_counts)
|
| 129 |
+
data_quality = round(1.0 - missing_total / total_fields, 3)
|
| 130 |
+
|
| 131 |
+
# Scale
|
| 132 |
+
scaled_np = scaler.transform(raw_np)
|
| 133 |
+
|
| 134 |
+
# Pad or truncate to SEQ_LEN=5
|
| 135 |
+
SEQ_LEN = 5
|
| 136 |
+
n = len(scaled_np)
|
| 137 |
+
if n < SEQ_LEN:
|
| 138 |
+
pad = np.zeros((SEQ_LEN - n, scaled_np.shape[1]), dtype=np.float32)
|
| 139 |
+
scaled_np = np.vstack([pad, scaled_np])
|
| 140 |
+
elif n > SEQ_LEN:
|
| 141 |
+
scaled_np = scaled_np[-SEQ_LEN:]
|
| 142 |
+
|
| 143 |
+
# Clinical safety net (hard WHO rules)
|
| 144 |
+
from api.alert_logic import apply_clinical_safety_net, AlertTier
|
| 145 |
+
|
| 146 |
+
visits_raw = [v.model_dump() for v in request.visits]
|
| 147 |
+
forced_tier, forced_reason = apply_clinical_safety_net(visits_raw)
|
| 148 |
+
|
| 149 |
+
# Run model and explain
|
| 150 |
+
explanation = explain_prediction(model, scaled_np, scaler, device)
|
| 151 |
+
|
| 152 |
+
if forced_tier is not None:
|
| 153 |
+
explanation["top_reasons"].insert(0, f"[WHO guideline] {forced_reason}")
|
| 154 |
+
|
| 155 |
+
# Alert tier
|
| 156 |
+
alert_tier, suppressed = compute_alert_tier(
|
| 157 |
+
probabilities = explanation["probabilities"],
|
| 158 |
+
patient_id = request.patient_id,
|
| 159 |
+
visit_importances= explanation["visit_importance"],
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Override with clinical rule minimum if needed
|
| 163 |
+
tier_order = {"GREEN": 0, "AMBER": 1, "RED": 2}
|
| 164 |
+
if forced_tier is not None:
|
| 165 |
+
if tier_order[forced_tier] > tier_order[alert_tier.value]:
|
| 166 |
+
alert_tier = AlertTier(forced_tier)
|
| 167 |
+
suppressed = False
|
| 168 |
+
|
| 169 |
+
# Action text
|
| 170 |
+
action, transfer_order = generate_action_text(
|
| 171 |
+
tier = alert_tier,
|
| 172 |
+
top_reasons = explanation["top_reasons"],
|
| 173 |
+
data_quality = data_quality,
|
| 174 |
+
staff_available = request.staff_available,
|
| 175 |
+
blood_units = request.blood_units,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
return PredictionResponse(
|
| 179 |
+
patient_id = request.patient_id,
|
| 180 |
+
risk_level = explanation["risk_level"],
|
| 181 |
+
alert_tier = alert_tier,
|
| 182 |
+
confidence = explanation["confidence"],
|
| 183 |
+
data_quality = data_quality,
|
| 184 |
+
top_reasons = explanation["top_reasons"],
|
| 185 |
+
feature_importance= explanation["feature_importance"],
|
| 186 |
+
visit_importance = explanation["visit_importance"],
|
| 187 |
+
action_required = action,
|
| 188 |
+
transfer_order = transfer_order,
|
| 189 |
+
suppressed = suppressed,
|
| 190 |
+
probabilities = explanation["probabilities"],
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.get("/stats")
|
| 195 |
+
async def get_stats():
|
| 196 |
+
"""Returns patient assessment statistics."""
|
| 197 |
+
from api.alert_logic import _alert_history
|
| 198 |
+
total = len(_alert_history)
|
| 199 |
+
by_tier = {"GREEN": 0, "AMBER": 0, "RED": 0}
|
| 200 |
+
for v in _alert_history.values():
|
| 201 |
+
by_tier[v["tier"].value] += 1
|
| 202 |
+
|
| 203 |
+
return {
|
| 204 |
+
"patients_assessed": total,
|
| 205 |
+
"by_tier": by_tier,
|
| 206 |
+
"model_version": "SheGuard-Mamba3-v1",
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# --- Dashboard static files ---------------------------------------------------
|
| 211 |
+
|
| 212 |
+
DASHBOARD_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "dashboard")
|
| 213 |
+
|
| 214 |
+
if os.path.isdir(DASHBOARD_DIR):
|
| 215 |
+
app.mount("/static", StaticFiles(directory=DASHBOARD_DIR), name="dashboard-static")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@app.get("/")
|
| 219 |
+
async def serve_dashboard():
|
| 220 |
+
"""Serve the dashboard HTML at root."""
|
| 221 |
+
index_path = os.path.join(DASHBOARD_DIR, "index.html")
|
| 222 |
+
if os.path.exists(index_path):
|
| 223 |
+
return FileResponse(index_path, media_type="text/html")
|
| 224 |
+
return {"status": "ok", "model_loaded": model is not None, "device": device}
|
api/schemas.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — API Schemas
|
| 3 |
+
Pydantic models for request/response validation.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from pydantic import BaseModel, Field, field_validator
|
| 7 |
+
from typing import Optional, List
|
| 8 |
+
from enum import Enum
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class RiskLevelEnum(str, Enum):
|
| 12 |
+
LOW = "Low risk"
|
| 13 |
+
MEDIUM = "Medium risk"
|
| 14 |
+
HIGH = "High risk"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PrenatalVisit(BaseModel):
|
| 18 |
+
"""One prenatal checkup reading."""
|
| 19 |
+
age: float = Field(..., ge=10, le=60, description="Patient age in years")
|
| 20 |
+
systolic_bp: float = Field(..., ge=70, le=200, description="Systolic blood pressure mmHg")
|
| 21 |
+
diastolic_bp: float = Field(..., ge=40, le=130, description="Diastolic blood pressure mmHg")
|
| 22 |
+
blood_sugar: Optional[float] = Field(None, ge=3.0, le=20.0, description="Blood glucose mmol/L")
|
| 23 |
+
body_temp: Optional[float] = Field(None, ge=35.0, le=42.0, description="Temperature °C")
|
| 24 |
+
heart_rate: Optional[float] = Field(None, ge=40, le=160, description="Heart rate bpm")
|
| 25 |
+
visit_date: Optional[str] = Field(None, description="ISO date of visit e.g. 2024-03-15")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class PredictionRequest(BaseModel):
|
| 29 |
+
"""Request body for POST /predict."""
|
| 30 |
+
patient_id: str = Field(..., description="Clinic's own patient ID")
|
| 31 |
+
visits: List[PrenatalVisit] = Field(
|
| 32 |
+
..., min_length=1, max_length=10,
|
| 33 |
+
description="Prenatal visit readings in chronological order (oldest first)"
|
| 34 |
+
)
|
| 35 |
+
staff_available: Optional[int] = Field(None, ge=0, description="Doctors on duty right now")
|
| 36 |
+
blood_units: Optional[int] = Field(None, ge=0, description="Units of O-negative blood in stock")
|
| 37 |
+
clinic_name: Optional[str] = Field(None, description="Clinic name for alert routing")
|
| 38 |
+
|
| 39 |
+
@field_validator("visits")
|
| 40 |
+
@classmethod
|
| 41 |
+
def at_least_one_visit(cls, v):
|
| 42 |
+
if len(v) < 1:
|
| 43 |
+
raise ValueError("At least 1 prenatal visit is required")
|
| 44 |
+
return v
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class AlertTier(str, Enum):
|
| 48 |
+
GREEN = "GREEN" # < 65% high risk confidence
|
| 49 |
+
AMBER = "AMBER" # 65–85% — watch closely
|
| 50 |
+
RED = "RED" # > 85% + 2+ consecutive high-risk visits
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class PredictionResponse(BaseModel):
|
| 54 |
+
"""Response from POST /predict."""
|
| 55 |
+
patient_id: str
|
| 56 |
+
risk_level: RiskLevelEnum
|
| 57 |
+
alert_tier: AlertTier
|
| 58 |
+
confidence: float
|
| 59 |
+
data_quality: float
|
| 60 |
+
top_reasons: List[str]
|
| 61 |
+
feature_importance: dict
|
| 62 |
+
visit_importance: List[float]
|
| 63 |
+
action_required: str
|
| 64 |
+
transfer_order: Optional[str] = None
|
| 65 |
+
suppressed: bool = False
|
| 66 |
+
probabilities: dict
|
dashboard/app.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ══════════════════════════════════════════════════════════════════════════
|
| 2 |
+
MamaGuard — Dashboard Application Logic
|
| 3 |
+
══════════════════════════════════════════════════════════════════════════ */
|
| 4 |
+
|
| 5 |
+
const API_BASE = "";
|
| 6 |
+
|
| 7 |
+
/* ── State ─────────────────────────────────────────────────────────────── */
|
| 8 |
+
|
| 9 |
+
let visitCount = 0;
|
| 10 |
+
let currentImageBase64 = null;
|
| 11 |
+
let currentImageType = null;
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
/* ── Visit Management ──────────────────────────────────────────────────── */
|
| 15 |
+
|
| 16 |
+
function addVisit(prefill = {}) {
|
| 17 |
+
visitCount++;
|
| 18 |
+
const container = document.getElementById("visits-container");
|
| 19 |
+
const block = document.createElement("div");
|
| 20 |
+
block.className = "visit-block";
|
| 21 |
+
block.id = `visit-${visitCount}`;
|
| 22 |
+
|
| 23 |
+
const v = visitCount;
|
| 24 |
+
block.innerHTML = `
|
| 25 |
+
<h4>Visit ${v}
|
| 26 |
+
${prefill.visit_date
|
| 27 |
+
? `<span style="font-weight:normal;color:var(--text-muted);font-size:0.78rem;margin-left:8px;">${prefill.visit_date}</span>`
|
| 28 |
+
: ""}
|
| 29 |
+
</h4>
|
| 30 |
+
<div class="grid-3">
|
| 31 |
+
<div>
|
| 32 |
+
<label>Age *</label>
|
| 33 |
+
<input type="number" id="v${v}_age" placeholder="e.g. 28" min="10" max="60">
|
| 34 |
+
</div>
|
| 35 |
+
<div>
|
| 36 |
+
<label>Systolic BP * (mmHg)</label>
|
| 37 |
+
<input type="number" id="v${v}_sbp" placeholder="e.g. 118" min="70" max="200">
|
| 38 |
+
</div>
|
| 39 |
+
<div>
|
| 40 |
+
<label>Diastolic BP * (mmHg)</label>
|
| 41 |
+
<input type="number" id="v${v}_dbp" placeholder="e.g. 75" min="40" max="130">
|
| 42 |
+
</div>
|
| 43 |
+
<div>
|
| 44 |
+
<label>Blood sugar (mmol/L)</label>
|
| 45 |
+
<input type="number" id="v${v}_bs" placeholder="e.g. 7.2" min="3" max="20" step="0.1">
|
| 46 |
+
</div>
|
| 47 |
+
<div>
|
| 48 |
+
<label>Body temp (°C)</label>
|
| 49 |
+
<input type="number" id="v${v}_bt" placeholder="e.g. 36.8" min="35" max="42" step="0.1">
|
| 50 |
+
</div>
|
| 51 |
+
<div>
|
| 52 |
+
<label>Heart rate (bpm)</label>
|
| 53 |
+
<input type="number" id="v${v}_hr" placeholder="e.g. 78" min="40" max="160">
|
| 54 |
+
</div>
|
| 55 |
+
</div>`;
|
| 56 |
+
container.appendChild(block);
|
| 57 |
+
|
| 58 |
+
if (Object.keys(prefill).length > 0) {
|
| 59 |
+
fillVisit(v, prefill);
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function fillVisit(v, data) {
|
| 64 |
+
const fieldMap = {
|
| 65 |
+
age: `v${v}_age`,
|
| 66 |
+
systolic_bp: `v${v}_sbp`,
|
| 67 |
+
diastolic_bp: `v${v}_dbp`,
|
| 68 |
+
blood_sugar: `v${v}_bs`,
|
| 69 |
+
body_temp: `v${v}_bt`,
|
| 70 |
+
heart_rate: `v${v}_hr`,
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
for (const [key, inputId] of Object.entries(fieldMap)) {
|
| 74 |
+
const val = data[key];
|
| 75 |
+
const el = document.getElementById(inputId);
|
| 76 |
+
if (el && val !== null && val !== undefined) {
|
| 77 |
+
el.value = val;
|
| 78 |
+
el.classList.add("autofilled");
|
| 79 |
+
el.addEventListener("input", () => el.classList.remove("autofilled"), { once: true });
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
/* ── File Upload ───────────────────────────────────────────────────────── */
|
| 86 |
+
|
| 87 |
+
function handleFileSelect(input) {
|
| 88 |
+
const file = input.files[0];
|
| 89 |
+
if (!file) return;
|
| 90 |
+
|
| 91 |
+
const reader = new FileReader();
|
| 92 |
+
reader.onload = (e) => {
|
| 93 |
+
const dataUrl = e.target.result;
|
| 94 |
+
const [header, base64] = dataUrl.split(",");
|
| 95 |
+
currentImageBase64 = base64;
|
| 96 |
+
currentImageType = file.type || "image/jpeg";
|
| 97 |
+
|
| 98 |
+
document.getElementById("preview-img").src = dataUrl;
|
| 99 |
+
document.getElementById("preview-wrap").style.display = "block";
|
| 100 |
+
document.getElementById("btn-extract").style.display = "block";
|
| 101 |
+
document.getElementById("extract-status").className = "extract-status";
|
| 102 |
+
document.getElementById("extract-status").textContent = "";
|
| 103 |
+
};
|
| 104 |
+
reader.readAsDataURL(file);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function clearUpload() {
|
| 108 |
+
currentImageBase64 = null;
|
| 109 |
+
currentImageType = null;
|
| 110 |
+
document.getElementById("file-input").value = "";
|
| 111 |
+
document.getElementById("preview-wrap").style.display = "none";
|
| 112 |
+
document.getElementById("btn-extract").style.display = "none";
|
| 113 |
+
document.getElementById("extract-status").className = "extract-status";
|
| 114 |
+
document.getElementById("extract-status").textContent = "";
|
| 115 |
+
document.getElementById("confidence-strip").style.display = "none";
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
/* ── Drag & Drop ───────────────────────────────────────────────────────── */
|
| 120 |
+
|
| 121 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 122 |
+
const zone = document.getElementById("upload-zone");
|
| 123 |
+
|
| 124 |
+
zone.addEventListener("dragover", (e) => {
|
| 125 |
+
e.preventDefault();
|
| 126 |
+
zone.classList.add("drag-over");
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
zone.addEventListener("dragleave", () => {
|
| 130 |
+
zone.classList.remove("drag-over");
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
zone.addEventListener("drop", (e) => {
|
| 134 |
+
e.preventDefault();
|
| 135 |
+
zone.classList.remove("drag-over");
|
| 136 |
+
const file = e.dataTransfer.files[0];
|
| 137 |
+
if (file) {
|
| 138 |
+
const dt = new DataTransfer();
|
| 139 |
+
dt.items.add(file);
|
| 140 |
+
document.getElementById("file-input").files = dt.files;
|
| 141 |
+
handleFileSelect(document.getElementById("file-input"));
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
// Add first visit on load
|
| 146 |
+
addVisit();
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
/* ── Image Extraction ──────────────────────────────────────────────────── */
|
| 151 |
+
|
| 152 |
+
async function extractFromImage() {
|
| 153 |
+
if (!currentImageBase64) return;
|
| 154 |
+
|
| 155 |
+
const statusEl = document.getElementById("extract-status");
|
| 156 |
+
const extractBtn = document.getElementById("btn-extract");
|
| 157 |
+
|
| 158 |
+
statusEl.className = "extract-status loading";
|
| 159 |
+
statusEl.textContent = "Reading the report... (2-5 seconds)";
|
| 160 |
+
extractBtn.disabled = true;
|
| 161 |
+
|
| 162 |
+
try {
|
| 163 |
+
const resp = await fetch(`${API_BASE}/extract-report`, {
|
| 164 |
+
method: "POST",
|
| 165 |
+
headers: { "Content-Type": "application/json" },
|
| 166 |
+
body: JSON.stringify({
|
| 167 |
+
image_base64: currentImageBase64,
|
| 168 |
+
image_type: currentImageType,
|
| 169 |
+
}),
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
if (!resp.ok) {
|
| 173 |
+
const err = await resp.json();
|
| 174 |
+
throw new Error(err.detail || `Server error ${resp.status}`);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const data = await resp.json();
|
| 178 |
+
|
| 179 |
+
if (data.patient_id) {
|
| 180 |
+
const pidField = document.getElementById("patient_id");
|
| 181 |
+
if (!pidField.value) {
|
| 182 |
+
pidField.value = data.patient_id;
|
| 183 |
+
pidField.classList.add("autofilled");
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
document.getElementById("visits-container").innerHTML = "";
|
| 188 |
+
visitCount = 0;
|
| 189 |
+
|
| 190 |
+
const visits = data.visits || [];
|
| 191 |
+
if (visits.length === 0) {
|
| 192 |
+
throw new Error("No visit data found in the image. Try a clearer photo.");
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
visits.forEach(visit => addVisit(visit));
|
| 196 |
+
|
| 197 |
+
const confPct = Math.round((data.confidence || 1.0) * 100);
|
| 198 |
+
const confColor = confPct >= 80 ? "var(--green)" : confPct >= 60 ? "var(--amber)" : "var(--red)";
|
| 199 |
+
const confEl = document.getElementById("confidence-strip");
|
| 200 |
+
confEl.style.display = "block";
|
| 201 |
+
confEl.innerHTML = `
|
| 202 |
+
<span style="color:${confColor};font-weight:600;">
|
| 203 |
+
Extraction confidence: ${confPct}%
|
| 204 |
+
</span>
|
| 205 |
+
${confPct < 80 ? " -- please verify highlighted fields before submitting" : " -- all fields extracted successfully"}
|
| 206 |
+
${data.notes ? `<br><span style="color:var(--text-muted);">${data.notes}</span>` : ""}
|
| 207 |
+
`;
|
| 208 |
+
|
| 209 |
+
statusEl.className = "extract-status success";
|
| 210 |
+
statusEl.textContent = `Extracted ${visits.length} visit${visits.length > 1 ? "s" : ""} -- highlighted fields are auto-filled. Verify before submitting.`;
|
| 211 |
+
|
| 212 |
+
} catch (err) {
|
| 213 |
+
statusEl.className = "extract-status error";
|
| 214 |
+
statusEl.textContent = `Extraction failed: ${err.message}`;
|
| 215 |
+
} finally {
|
| 216 |
+
extractBtn.disabled = false;
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
/* ── Prediction Submission ─────────────────────────────────────────────── */
|
| 222 |
+
|
| 223 |
+
function getVal(id) {
|
| 224 |
+
const v = document.getElementById(id)?.value;
|
| 225 |
+
return (v === "" || v === null || v === undefined) ? null : parseFloat(v);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
async function submitPrediction() {
|
| 229 |
+
document.getElementById("result-card").style.display = "none";
|
| 230 |
+
document.getElementById("error-box").style.display = "none";
|
| 231 |
+
document.getElementById("loader").style.display = "block";
|
| 232 |
+
|
| 233 |
+
const patientId = document.getElementById("patient_id").value.trim();
|
| 234 |
+
if (!patientId) { showError("Please enter a Patient ID"); return; }
|
| 235 |
+
|
| 236 |
+
const visits = [];
|
| 237 |
+
for (let i = 1; i <= visitCount; i++) {
|
| 238 |
+
const age = getVal(`v${i}_age`);
|
| 239 |
+
const sbp = getVal(`v${i}_sbp`);
|
| 240 |
+
const dbp = getVal(`v${i}_dbp`);
|
| 241 |
+
if (age === null || sbp === null || dbp === null) continue;
|
| 242 |
+
visits.push({
|
| 243 |
+
age, systolic_bp: sbp, diastolic_bp: dbp,
|
| 244 |
+
blood_sugar: getVal(`v${i}_bs`),
|
| 245 |
+
body_temp: getVal(`v${i}_bt`),
|
| 246 |
+
heart_rate: getVal(`v${i}_hr`),
|
| 247 |
+
});
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if (visits.length === 0) {
|
| 251 |
+
showError("Please fill in at least one visit (Age, Systolic BP, Diastolic BP are required)");
|
| 252 |
+
return;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
const payload = {
|
| 256 |
+
patient_id: patientId,
|
| 257 |
+
visits,
|
| 258 |
+
staff_available: getVal("staff_available"),
|
| 259 |
+
blood_units: getVal("blood_units"),
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
try {
|
| 263 |
+
const resp = await fetch(`${API_BASE}/predict`, {
|
| 264 |
+
method: "POST",
|
| 265 |
+
headers: { "Content-Type": "application/json" },
|
| 266 |
+
body: JSON.stringify(payload),
|
| 267 |
+
});
|
| 268 |
+
if (!resp.ok) {
|
| 269 |
+
const err = await resp.json();
|
| 270 |
+
throw new Error(err.detail || `Server error ${resp.status}`);
|
| 271 |
+
}
|
| 272 |
+
showResult(await resp.json());
|
| 273 |
+
} catch (err) {
|
| 274 |
+
showError(`Could not reach the server: ${err.message}. Is the API running?`);
|
| 275 |
+
} finally {
|
| 276 |
+
document.getElementById("loader").style.display = "none";
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
/* ── Result Display ────────────────────────────────────────────────────── */
|
| 282 |
+
|
| 283 |
+
function showResult(data) {
|
| 284 |
+
const tierClass = { GREEN: "alert-green", AMBER: "alert-amber", RED: "alert-red" };
|
| 285 |
+
const badgeClass = { GREEN: "badge-green", AMBER: "badge-amber", RED: "badge-red" };
|
| 286 |
+
const tierLabel = {
|
| 287 |
+
GREEN: "Low risk",
|
| 288 |
+
AMBER: "Medium risk -- monitor closely",
|
| 289 |
+
RED: "High risk -- refer immediately"
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
const card = document.getElementById("result-card");
|
| 293 |
+
const tier = data.alert_tier;
|
| 294 |
+
|
| 295 |
+
card.className = `card result ${tierClass[tier]}`;
|
| 296 |
+
card.style.display = "block";
|
| 297 |
+
|
| 298 |
+
document.getElementById("risk-badge").textContent = tierLabel[tier];
|
| 299 |
+
document.getElementById("risk-badge").className = `risk-badge ${badgeClass[tier]}`;
|
| 300 |
+
|
| 301 |
+
document.getElementById("confidence-text").textContent =
|
| 302 |
+
`Model confidence: ${(data.confidence * 100).toFixed(1)}%` +
|
| 303 |
+
(data.suppressed ? " (alert suppressed -- already sent within 48h)" : "");
|
| 304 |
+
|
| 305 |
+
const qPct = Math.round(data.data_quality * 100);
|
| 306 |
+
document.getElementById("quality-bar").style.width = `${qPct}%`;
|
| 307 |
+
document.getElementById("quality-bar").style.background = qPct >= 70 ? "var(--green)" : "var(--amber)";
|
| 308 |
+
document.getElementById("quality-text").textContent =
|
| 309 |
+
`Data quality: ${qPct}%` + (qPct < 70 ? " -- low confidence, collect missing readings" : " -- good");
|
| 310 |
+
|
| 311 |
+
document.getElementById("reasons-list").innerHTML =
|
| 312 |
+
data.top_reasons.map(r => `<div class="reason-item">${r}</div>`).join("");
|
| 313 |
+
|
| 314 |
+
document.getElementById("action-text").textContent = data.action_required;
|
| 315 |
+
|
| 316 |
+
const transferBox = document.getElementById("transfer-box");
|
| 317 |
+
if (data.transfer_order) {
|
| 318 |
+
document.getElementById("transfer-text").textContent = data.transfer_order;
|
| 319 |
+
transferBox.style.display = "block";
|
| 320 |
+
} else {
|
| 321 |
+
transferBox.style.display = "none";
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
card.scrollIntoView({ behavior: "smooth" });
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
function showError(msg) {
|
| 328 |
+
document.getElementById("loader").style.display = "none";
|
| 329 |
+
const box = document.getElementById("error-box");
|
| 330 |
+
box.textContent = msg;
|
| 331 |
+
box.style.display = "block";
|
| 332 |
+
}
|
dashboard/index.html
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<meta name="description" content="MamaGuard maternal risk assessment dashboard powered by Mamba3 AI">
|
| 8 |
+
<title>MamaGuard — Maternal Risk Assessment</title>
|
| 9 |
+
<link rel="stylesheet" href="/static/styles.css">
|
| 10 |
+
</head>
|
| 11 |
+
|
| 12 |
+
<body>
|
| 13 |
+
<div class="app-container">
|
| 14 |
+
|
| 15 |
+
<!-- Header -->
|
| 16 |
+
<header class="app-header">
|
| 17 |
+
<h1>MamaGuard</h1>
|
| 18 |
+
<p class="subtitle">Maternal risk assessment — powered by Mamba3 AI</p>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<!-- Patient Details -->
|
| 22 |
+
<section class="card" id="patient-section">
|
| 23 |
+
<h3 class="card-title">Patient details</h3>
|
| 24 |
+
<p class="card-desc">Fields marked * are required. Leave others blank if not measured.</p>
|
| 25 |
+
|
| 26 |
+
<label for="patient_id">Patient ID *</label>
|
| 27 |
+
<input type="text" id="patient_id" placeholder="e.g. PT-2024-0045" required>
|
| 28 |
+
|
| 29 |
+
<label for="staff_available">Clinic staff on duty</label>
|
| 30 |
+
<input type="number" id="staff_available" placeholder="e.g. 2" min="0">
|
| 31 |
+
|
| 32 |
+
<label for="blood_units">O-negative blood units in stock</label>
|
| 33 |
+
<input type="number" id="blood_units" placeholder="e.g. 4" min="0">
|
| 34 |
+
</section>
|
| 35 |
+
|
| 36 |
+
<!-- Upload Zone -->
|
| 37 |
+
<section class="card" id="upload-section">
|
| 38 |
+
<h3 class="card-title">Auto-fill from report photo</h3>
|
| 39 |
+
<p class="card-desc">
|
| 40 |
+
Upload a photo or scan of the prenatal record card.
|
| 41 |
+
AI will read it and fill in all visit fields automatically.
|
| 42 |
+
</p>
|
| 43 |
+
|
| 44 |
+
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-input').click()">
|
| 45 |
+
<span class="upload-icon">📷</span>
|
| 46 |
+
<div class="upload-title">Tap to upload or drag a photo here</div>
|
| 47 |
+
<div class="upload-sub">JPEG, PNG, PDF — photo of paper report, printed form, or screenshot</div>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<input type="file" id="file-input" accept="image/*,.pdf" onchange="handleFileSelect(this)">
|
| 51 |
+
|
| 52 |
+
<div class="preview-wrap" id="preview-wrap">
|
| 53 |
+
<img id="preview-img" src="" alt="Report preview">
|
| 54 |
+
<button class="preview-remove" onclick="clearUpload()" title="Remove">×</button>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div class="confidence-strip" id="confidence-strip"></div>
|
| 58 |
+
|
| 59 |
+
<button class="btn-extract" id="btn-extract" onclick="extractFromImage()">
|
| 60 |
+
Extract data from image
|
| 61 |
+
</button>
|
| 62 |
+
|
| 63 |
+
<div class="extract-status" id="extract-status"></div>
|
| 64 |
+
</section>
|
| 65 |
+
|
| 66 |
+
<!-- Visits -->
|
| 67 |
+
<section class="card" id="visits-section">
|
| 68 |
+
<h3 class="card-title">Prenatal visits (oldest first)</h3>
|
| 69 |
+
<p class="card-desc">Add visits manually, or use the photo upload above to auto-fill.</p>
|
| 70 |
+
|
| 71 |
+
<div id="visits-container"></div>
|
| 72 |
+
<button class="btn-add" onclick="addVisit()">+ Add another visit</button>
|
| 73 |
+
</section>
|
| 74 |
+
|
| 75 |
+
<!-- Submit -->
|
| 76 |
+
<button class="btn-primary" id="btn-submit" onclick="submitPrediction()">
|
| 77 |
+
Assess maternal risk
|
| 78 |
+
</button>
|
| 79 |
+
|
| 80 |
+
<div class="loader" id="loader">Analysing patient data...</div>
|
| 81 |
+
<div class="error-box" id="error-box"></div>
|
| 82 |
+
|
| 83 |
+
<!-- Results -->
|
| 84 |
+
<section class="card result" id="result-card">
|
| 85 |
+
<div id="alert-header">
|
| 86 |
+
<span id="risk-badge" class="risk-badge"></span>
|
| 87 |
+
<p id="confidence-text" style="font-size:0.84rem;color:var(--text-muted);margin-bottom:10px;"></p>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<p class="section-label">Data quality</p>
|
| 91 |
+
<div class="quality-bar-wrap">
|
| 92 |
+
<div class="quality-bar" id="quality-bar" style="background:var(--primary);width:0%"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<p id="quality-text" class="section-label-sm"></p>
|
| 95 |
+
|
| 96 |
+
<p class="section-label" style="margin-top:14px;margin-bottom:6px;">Why this risk level:</p>
|
| 97 |
+
<div id="reasons-list"></div>
|
| 98 |
+
|
| 99 |
+
<p class="section-label" style="margin-top:14px;margin-bottom:6px;">Action required:</p>
|
| 100 |
+
<p id="action-text" style="font-size:0.92rem;line-height:1.5;"></p>
|
| 101 |
+
|
| 102 |
+
<div class="transfer-box" id="transfer-box" style="display:none;">
|
| 103 |
+
<strong>Transfer order:</strong>
|
| 104 |
+
<p id="transfer-text" style="margin-top:6px;"></p>
|
| 105 |
+
</div>
|
| 106 |
+
</section>
|
| 107 |
+
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<script src="/static/app.js"></script>
|
| 111 |
+
</body>
|
| 112 |
+
|
| 113 |
+
</html>
|
dashboard/styles.css
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ══════════════════════════════════════════════════════════════════════════
|
| 2 |
+
MamaGuard — Dashboard Styles
|
| 3 |
+
══════════════════════════════════════════════════════════════════════════ */
|
| 4 |
+
|
| 5 |
+
/* ── Reset & Custom Properties ─────────────────────────────────────────── */
|
| 6 |
+
|
| 7 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 8 |
+
|
| 9 |
+
:root {
|
| 10 |
+
--primary: #8B2252;
|
| 11 |
+
--primary-light: #a83a6a;
|
| 12 |
+
--primary-pale: #f9eff4;
|
| 13 |
+
--primary-bg: #fdf8fb;
|
| 14 |
+
--accent: #5b1a6e;
|
| 15 |
+
--surface: rgba(255, 255, 255, 0.72);
|
| 16 |
+
--surface-solid: #ffffff;
|
| 17 |
+
--bg: #f3eef0;
|
| 18 |
+
--text: #2d2d2d;
|
| 19 |
+
--text-muted: #777;
|
| 20 |
+
--border: #e8dce2;
|
| 21 |
+
--green: #2e7d32;
|
| 22 |
+
--green-bg: #e8f5e9;
|
| 23 |
+
--amber: #e65100;
|
| 24 |
+
--amber-bg: #fff3e0;
|
| 25 |
+
--red: #c62828;
|
| 26 |
+
--red-bg: #ffebee;
|
| 27 |
+
--radius: 14px;
|
| 28 |
+
--radius-sm: 8px;
|
| 29 |
+
--shadow: 0 4px 24px rgba(139, 34, 82, 0.08);
|
| 30 |
+
--shadow-hover: 0 8px 32px rgba(139, 34, 82, 0.14);
|
| 31 |
+
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
*,
|
| 35 |
+
*::before,
|
| 36 |
+
*::after {
|
| 37 |
+
box-sizing: border-box;
|
| 38 |
+
margin: 0;
|
| 39 |
+
padding: 0;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
body {
|
| 43 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
| 44 |
+
background: var(--bg);
|
| 45 |
+
color: var(--text);
|
| 46 |
+
line-height: 1.5;
|
| 47 |
+
min-height: 100vh;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* ── Layout Container ──────────────────────────────────────────────────── */
|
| 51 |
+
|
| 52 |
+
.app-container {
|
| 53 |
+
max-width: 720px;
|
| 54 |
+
margin: 0 auto;
|
| 55 |
+
padding: 24px 16px 48px;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* ── Header ────────────────────────────────────────────────────────────── */
|
| 59 |
+
|
| 60 |
+
.app-header {
|
| 61 |
+
text-align: center;
|
| 62 |
+
margin-bottom: 28px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.app-header h1 {
|
| 66 |
+
font-size: 1.75rem;
|
| 67 |
+
font-weight: 700;
|
| 68 |
+
color: var(--primary);
|
| 69 |
+
letter-spacing: -0.02em;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.app-header .subtitle {
|
| 73 |
+
color: var(--text-muted);
|
| 74 |
+
font-size: 0.9rem;
|
| 75 |
+
margin-top: 4px;
|
| 76 |
+
font-weight: 400;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* ── Cards (glassmorphism) ─────────────────────────────────────────────── */
|
| 80 |
+
|
| 81 |
+
.card {
|
| 82 |
+
background: var(--surface);
|
| 83 |
+
backdrop-filter: blur(12px);
|
| 84 |
+
-webkit-backdrop-filter: blur(12px);
|
| 85 |
+
border: 1px solid var(--border);
|
| 86 |
+
border-radius: var(--radius);
|
| 87 |
+
padding: 24px;
|
| 88 |
+
margin-bottom: 20px;
|
| 89 |
+
box-shadow: var(--shadow);
|
| 90 |
+
transition: box-shadow var(--transition);
|
| 91 |
+
animation: fadeSlideUp 0.4s ease both;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.card:hover {
|
| 95 |
+
box-shadow: var(--shadow-hover);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.card-title {
|
| 99 |
+
color: var(--primary);
|
| 100 |
+
font-size: 1rem;
|
| 101 |
+
font-weight: 600;
|
| 102 |
+
margin-bottom: 4px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.card-desc {
|
| 106 |
+
font-size: 0.8rem;
|
| 107 |
+
color: var(--text-muted);
|
| 108 |
+
margin-bottom: 16px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* ── Form Elements ─────────────────────────────────────────────────────── */
|
| 112 |
+
|
| 113 |
+
label {
|
| 114 |
+
display: block;
|
| 115 |
+
font-weight: 500;
|
| 116 |
+
font-size: 0.82rem;
|
| 117 |
+
margin-bottom: 4px;
|
| 118 |
+
color: #555;
|
| 119 |
+
margin-top: 14px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
input {
|
| 123 |
+
width: 100%;
|
| 124 |
+
padding: 10px 14px;
|
| 125 |
+
border: 1.5px solid var(--border);
|
| 126 |
+
border-radius: var(--radius-sm);
|
| 127 |
+
font-family: inherit;
|
| 128 |
+
font-size: 0.92rem;
|
| 129 |
+
background: var(--surface-solid);
|
| 130 |
+
transition: border-color var(--transition), box-shadow var(--transition);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
input:focus {
|
| 134 |
+
outline: none;
|
| 135 |
+
border-color: var(--primary);
|
| 136 |
+
box-shadow: 0 0 0 3px rgba(139, 34, 82, 0.12);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
input::placeholder {
|
| 140 |
+
color: #bbb;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* ── Visit Blocks ──────────────────────────────────────────────────────── */
|
| 144 |
+
|
| 145 |
+
.visit-block {
|
| 146 |
+
border: 1.5px solid var(--border);
|
| 147 |
+
border-radius: var(--radius-sm);
|
| 148 |
+
padding: 16px;
|
| 149 |
+
margin-top: 14px;
|
| 150 |
+
background: var(--primary-bg);
|
| 151 |
+
position: relative;
|
| 152 |
+
animation: fadeSlideUp 0.3s ease both;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.visit-block h4 {
|
| 156 |
+
color: var(--primary);
|
| 157 |
+
font-size: 0.88rem;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
margin-bottom: 10px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.grid-3 {
|
| 163 |
+
display: grid;
|
| 164 |
+
grid-template-columns: 1fr 1fr 1fr;
|
| 165 |
+
gap: 12px;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* ── Buttons ───────────────────────────────────────────────────────────── */
|
| 169 |
+
|
| 170 |
+
button {
|
| 171 |
+
font-family: inherit;
|
| 172 |
+
padding: 12px 24px;
|
| 173 |
+
border: none;
|
| 174 |
+
border-radius: var(--radius-sm);
|
| 175 |
+
cursor: pointer;
|
| 176 |
+
font-size: 0.95rem;
|
| 177 |
+
font-weight: 600;
|
| 178 |
+
transition: all var(--transition);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.btn-primary {
|
| 182 |
+
background: linear-gradient(135deg, var(--primary), var(--accent));
|
| 183 |
+
color: white;
|
| 184 |
+
width: 100%;
|
| 185 |
+
margin-top: 20px;
|
| 186 |
+
padding: 14px;
|
| 187 |
+
font-size: 1rem;
|
| 188 |
+
border-radius: var(--radius);
|
| 189 |
+
box-shadow: 0 4px 16px rgba(139, 34, 82, 0.25);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.btn-primary:hover {
|
| 193 |
+
transform: translateY(-1px);
|
| 194 |
+
box-shadow: 0 6px 24px rgba(139, 34, 82, 0.35);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.btn-primary:active {
|
| 198 |
+
transform: translateY(0);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.btn-add {
|
| 202 |
+
background: var(--primary-pale);
|
| 203 |
+
color: var(--primary);
|
| 204 |
+
margin-top: 10px;
|
| 205 |
+
font-size: 0.82rem;
|
| 206 |
+
padding: 10px 18px;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.btn-add:hover {
|
| 210 |
+
background: #f0dde6;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* ── Upload Zone ───────────────────────────────────────────────────────── */
|
| 214 |
+
|
| 215 |
+
.upload-zone {
|
| 216 |
+
border: 2px dashed #c9a0b4;
|
| 217 |
+
border-radius: var(--radius);
|
| 218 |
+
padding: 32px 20px;
|
| 219 |
+
text-align: center;
|
| 220 |
+
cursor: pointer;
|
| 221 |
+
transition: all var(--transition);
|
| 222 |
+
background: var(--primary-bg);
|
| 223 |
+
margin-bottom: 16px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.upload-zone:hover,
|
| 227 |
+
.upload-zone.drag-over {
|
| 228 |
+
border-color: var(--primary);
|
| 229 |
+
background: var(--primary-pale);
|
| 230 |
+
transform: scale(1.01);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.upload-zone .upload-icon {
|
| 234 |
+
font-size: 2.5rem;
|
| 235 |
+
display: block;
|
| 236 |
+
margin-bottom: 8px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.upload-zone .upload-title {
|
| 240 |
+
font-weight: 600;
|
| 241 |
+
color: var(--primary);
|
| 242 |
+
font-size: 1rem;
|
| 243 |
+
margin-bottom: 4px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.upload-zone .upload-sub {
|
| 247 |
+
font-size: 0.78rem;
|
| 248 |
+
color: var(--text-muted);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
#file-input {
|
| 252 |
+
display: none;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* ── Image Preview ─────────────────────────────────────────────────────── */
|
| 256 |
+
|
| 257 |
+
.preview-wrap {
|
| 258 |
+
display: none;
|
| 259 |
+
margin-top: 14px;
|
| 260 |
+
position: relative;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.preview-wrap img {
|
| 264 |
+
max-width: 100%;
|
| 265 |
+
max-height: 200px;
|
| 266 |
+
border-radius: var(--radius-sm);
|
| 267 |
+
border: 1.5px solid var(--border);
|
| 268 |
+
display: block;
|
| 269 |
+
margin: 0 auto;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.preview-remove {
|
| 273 |
+
position: absolute;
|
| 274 |
+
top: 6px;
|
| 275 |
+
right: 6px;
|
| 276 |
+
background: var(--red);
|
| 277 |
+
color: white;
|
| 278 |
+
border: none;
|
| 279 |
+
border-radius: 50%;
|
| 280 |
+
width: 28px;
|
| 281 |
+
height: 28px;
|
| 282 |
+
font-size: 0.8rem;
|
| 283 |
+
cursor: pointer;
|
| 284 |
+
display: flex;
|
| 285 |
+
align-items: center;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
padding: 0;
|
| 288 |
+
transition: transform var(--transition);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.preview-remove:hover {
|
| 292 |
+
transform: scale(1.1);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/* ── Extract Button & Status ───────────────────────────────────────────── */
|
| 296 |
+
|
| 297 |
+
.btn-extract {
|
| 298 |
+
background: linear-gradient(135deg, var(--accent), #7b2d8e);
|
| 299 |
+
color: white;
|
| 300 |
+
width: 100%;
|
| 301 |
+
margin-top: 12px;
|
| 302 |
+
display: none;
|
| 303 |
+
font-size: 0.88rem;
|
| 304 |
+
padding: 12px;
|
| 305 |
+
border-radius: var(--radius-sm);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.btn-extract:hover {
|
| 309 |
+
transform: translateY(-1px);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.extract-status {
|
| 313 |
+
display: none;
|
| 314 |
+
margin-top: 12px;
|
| 315 |
+
padding: 12px 16px;
|
| 316 |
+
border-radius: var(--radius-sm);
|
| 317 |
+
font-size: 0.84rem;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.extract-status.loading {
|
| 321 |
+
background: #f3e5f5;
|
| 322 |
+
color: #6a1b9a;
|
| 323 |
+
display: block;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.extract-status.success {
|
| 327 |
+
background: var(--green-bg);
|
| 328 |
+
color: var(--green);
|
| 329 |
+
display: block;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.extract-status.error {
|
| 333 |
+
background: var(--red-bg);
|
| 334 |
+
color: var(--red);
|
| 335 |
+
display: block;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/* ── Autofilled Indicator ──────────────────────────────────────────────── */
|
| 339 |
+
|
| 340 |
+
.autofilled {
|
| 341 |
+
border-color: var(--primary) !important;
|
| 342 |
+
background: #fdf0f5 !important;
|
| 343 |
+
box-shadow: 0 0 0 2px rgba(139, 34, 82, 0.1) !important;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.confidence-strip {
|
| 347 |
+
display: none;
|
| 348 |
+
margin-top: 10px;
|
| 349 |
+
padding: 10px 14px;
|
| 350 |
+
background: var(--primary-pale);
|
| 351 |
+
border-radius: var(--radius-sm);
|
| 352 |
+
font-size: 0.8rem;
|
| 353 |
+
color: #555;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/* ── Results Card ──────────────────────────────────────────────────────── */
|
| 357 |
+
|
| 358 |
+
.result {
|
| 359 |
+
display: none;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.alert-green {
|
| 363 |
+
border-left: 5px solid var(--green);
|
| 364 |
+
background: rgba(232, 245, 233, 0.7);
|
| 365 |
+
backdrop-filter: blur(8px);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.alert-amber {
|
| 369 |
+
border-left: 5px solid var(--amber);
|
| 370 |
+
background: rgba(255, 243, 224, 0.7);
|
| 371 |
+
backdrop-filter: blur(8px);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.alert-red {
|
| 375 |
+
border-left: 5px solid var(--red);
|
| 376 |
+
background: rgba(255, 235, 238, 0.7);
|
| 377 |
+
backdrop-filter: blur(8px);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.risk-badge {
|
| 381 |
+
display: inline-block;
|
| 382 |
+
padding: 8px 20px;
|
| 383 |
+
border-radius: 24px;
|
| 384 |
+
font-weight: 700;
|
| 385 |
+
font-size: 1.05rem;
|
| 386 |
+
margin-bottom: 14px;
|
| 387 |
+
letter-spacing: -0.01em;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.badge-green {
|
| 391 |
+
background: #c8e6c9;
|
| 392 |
+
color: #1b5e20;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.badge-amber {
|
| 396 |
+
background: #ffe0b2;
|
| 397 |
+
color: #bf360c;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.badge-red {
|
| 401 |
+
background: #ffcdd2;
|
| 402 |
+
color: #b71c1c;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
/* ── Quality Bar ───────────────────────────────────────────────────────── */
|
| 406 |
+
|
| 407 |
+
.quality-bar-wrap {
|
| 408 |
+
background: #eee;
|
| 409 |
+
border-radius: 4px;
|
| 410 |
+
height: 8px;
|
| 411 |
+
margin: 6px 0 10px;
|
| 412 |
+
overflow: hidden;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.quality-bar {
|
| 416 |
+
height: 8px;
|
| 417 |
+
border-radius: 4px;
|
| 418 |
+
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* ── Reason Items ──────────────────────────────────────────────────────── */
|
| 422 |
+
|
| 423 |
+
.reason-item {
|
| 424 |
+
padding: 8px 12px;
|
| 425 |
+
background: var(--primary-pale);
|
| 426 |
+
border-radius: var(--radius-sm);
|
| 427 |
+
margin-bottom: 6px;
|
| 428 |
+
font-size: 0.88rem;
|
| 429 |
+
line-height: 1.4;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
/* ── Transfer Box ──────────────────────────────────────────────────────── */
|
| 433 |
+
|
| 434 |
+
.transfer-box {
|
| 435 |
+
background: var(--amber-bg);
|
| 436 |
+
border: 1.5px solid #ff9800;
|
| 437 |
+
border-radius: var(--radius-sm);
|
| 438 |
+
padding: 14px;
|
| 439 |
+
margin-top: 14px;
|
| 440 |
+
font-size: 0.88rem;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
/* ── Loader & Error ────────────────────────────────────────────────────── */
|
| 444 |
+
|
| 445 |
+
.loader {
|
| 446 |
+
display: none;
|
| 447 |
+
text-align: center;
|
| 448 |
+
padding: 24px;
|
| 449 |
+
color: var(--primary);
|
| 450 |
+
font-size: 0.95rem;
|
| 451 |
+
font-weight: 500;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.error-box {
|
| 455 |
+
background: var(--red-bg);
|
| 456 |
+
border: 1.5px solid #ef9a9a;
|
| 457 |
+
border-radius: var(--radius-sm);
|
| 458 |
+
padding: 14px;
|
| 459 |
+
color: var(--red);
|
| 460 |
+
display: none;
|
| 461 |
+
margin-top: 16px;
|
| 462 |
+
font-size: 0.9rem;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
/* ── Section Labels ────────────────────────────────────────────────────── */
|
| 466 |
+
|
| 467 |
+
.section-label {
|
| 468 |
+
font-size: 0.82rem;
|
| 469 |
+
font-weight: 600;
|
| 470 |
+
color: #555;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.section-label-sm {
|
| 474 |
+
font-size: 0.78rem;
|
| 475 |
+
color: var(--text-muted);
|
| 476 |
+
margin-bottom: 8px;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
/* ── Animations ────────────────────────────────────────────────────────── */
|
| 480 |
+
|
| 481 |
+
@keyframes fadeSlideUp {
|
| 482 |
+
from {
|
| 483 |
+
opacity: 0;
|
| 484 |
+
transform: translateY(12px);
|
| 485 |
+
}
|
| 486 |
+
to {
|
| 487 |
+
opacity: 1;
|
| 488 |
+
transform: translateY(0);
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.card:nth-child(1) { animation-delay: 0s; }
|
| 493 |
+
.card:nth-child(2) { animation-delay: 0.08s; }
|
| 494 |
+
.card:nth-child(3) { animation-delay: 0.16s; }
|
| 495 |
+
|
| 496 |
+
/* ── Responsive ────────────────────────────────────────────────────────── */
|
| 497 |
+
|
| 498 |
+
@media (max-width: 640px) {
|
| 499 |
+
.grid-3 {
|
| 500 |
+
grid-template-columns: 1fr 1fr;
|
| 501 |
+
gap: 10px;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.app-container {
|
| 505 |
+
padding: 16px 12px 32px;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card {
|
| 509 |
+
padding: 18px;
|
| 510 |
+
}
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
@media (max-width: 400px) {
|
| 514 |
+
.grid-3 {
|
| 515 |
+
grid-template-columns: 1fr;
|
| 516 |
+
}
|
| 517 |
+
}
|
data/maternal_health.csv
ADDED
|
@@ -0,0 +1,1015 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Age,SystolicBP,DiastolicBP,BS,BodyTemp,HeartRate,RiskLevel
|
| 2 |
+
25,130,80,15,98,86,high risk
|
| 3 |
+
35,140,90,13,98,70,high risk
|
| 4 |
+
29,90,70,8,100,80,high risk
|
| 5 |
+
30,140,85,7,98,70,high risk
|
| 6 |
+
35,120,60,6.1,98,76,low risk
|
| 7 |
+
23,140,80,7.01,98,70,high risk
|
| 8 |
+
23,130,70,7.01,98,78,mid risk
|
| 9 |
+
35,85,60,11,102,86,high risk
|
| 10 |
+
32,120,90,6.9,98,70,mid risk
|
| 11 |
+
42,130,80,18,98,70,high risk
|
| 12 |
+
23,90,60,7.01,98,76,low risk
|
| 13 |
+
19,120,80,7,98,70,mid risk
|
| 14 |
+
25,110,89,7.01,98,77,low risk
|
| 15 |
+
20,120,75,7.01,100,70,mid risk
|
| 16 |
+
48,120,80,11,98,88,mid risk
|
| 17 |
+
15,120,80,7.01,98,70,low risk
|
| 18 |
+
50,140,90,15,98,90,high risk
|
| 19 |
+
25,140,100,7.01,98,80,high risk
|
| 20 |
+
30,120,80,6.9,101,76,mid risk
|
| 21 |
+
10,70,50,6.9,98,70,low risk
|
| 22 |
+
40,140,100,18,98,90,high risk
|
| 23 |
+
50,140,80,6.7,98,70,mid risk
|
| 24 |
+
21,90,65,7.5,98,76,low risk
|
| 25 |
+
18,90,60,7.5,98,70,low risk
|
| 26 |
+
21,120,80,7.5,98,76,low risk
|
| 27 |
+
16,100,70,7.2,98,80,low risk
|
| 28 |
+
19,120,75,7.2,98,66,low risk
|
| 29 |
+
22,100,65,7.2,98,70,low risk
|
| 30 |
+
49,120,90,7.2,98,77,low risk
|
| 31 |
+
28,90,60,7.2,98,82,low risk
|
| 32 |
+
20,100,90,7.1,98,88,low risk
|
| 33 |
+
23,100,85,7.1,98,66,low risk
|
| 34 |
+
22,120,90,7.1,98,82,low risk
|
| 35 |
+
21,120,80,7.1,98,77,low risk
|
| 36 |
+
21,75,50,6.1,98,70,low risk
|
| 37 |
+
12,95,60,6.1,102,60,low risk
|
| 38 |
+
60,120,80,6.1,98,75,low risk
|
| 39 |
+
55,100,65,6.1,98,66,low risk
|
| 40 |
+
45,120,95,6.1,98,66,low risk
|
| 41 |
+
35,100,70,6.1,98,66,low risk
|
| 42 |
+
22,120,85,6.1,98,88,low risk
|
| 43 |
+
23,120,90,6.1,98,60,low risk
|
| 44 |
+
25,90,70,6.1,98,80,low risk
|
| 45 |
+
30,120,80,6.1,98,70,low risk
|
| 46 |
+
23,120,90,6.1,98,70,low risk
|
| 47 |
+
32,120,90,7.5,98,70,low risk
|
| 48 |
+
42,120,80,7.5,98,70,low risk
|
| 49 |
+
23,90,60,7.5,98,76,low risk
|
| 50 |
+
15,76,49,7.5,98,77,low risk
|
| 51 |
+
15,120,80,7,98,70,low risk
|
| 52 |
+
25,120,80,7,98,66,low risk
|
| 53 |
+
22,100,65,7,98,80,low risk
|
| 54 |
+
35,100,70,7,98,60,low risk
|
| 55 |
+
19,120,85,7,98,60,low risk
|
| 56 |
+
60,90,65,7,98,77,low risk
|
| 57 |
+
23,120,90,6.7,98,70,low risk
|
| 58 |
+
32,120,90,6.4,98,70,low risk
|
| 59 |
+
42,120,80,6.4,98,70,low risk
|
| 60 |
+
23,90,60,6.4,98,76,low risk
|
| 61 |
+
15,76,49,6.4,98,77,low risk
|
| 62 |
+
15,120,80,7.2,98,70,low risk
|
| 63 |
+
15,80,60,7,98,80,low risk
|
| 64 |
+
12,95,60,7.2,98,77,low risk
|
| 65 |
+
29,90,70,6.7,98,80,mid risk
|
| 66 |
+
31,120,60,6.1,98,76,mid risk
|
| 67 |
+
29,130,70,6.7,98,78,mid risk
|
| 68 |
+
17,85,60,9,102,86,mid risk
|
| 69 |
+
19,120,80,7,98,70,mid risk
|
| 70 |
+
20,110,60,7,100,70,mid risk
|
| 71 |
+
32,120,65,6,101,76,mid risk
|
| 72 |
+
26,85,60,6,101,86,mid risk
|
| 73 |
+
29,130,70,7.7,98,78,mid risk
|
| 74 |
+
19,120,80,7,98,70,mid risk
|
| 75 |
+
54,130,70,12,98,67,mid risk
|
| 76 |
+
44,120,90,16,98,80,mid risk
|
| 77 |
+
23,130,70,6.9,98,70,mid risk
|
| 78 |
+
22,85,60,6.9,98,76,mid risk
|
| 79 |
+
55,120,90,12,98,70,mid risk
|
| 80 |
+
35,120,80,6.9,98,78,mid risk
|
| 81 |
+
21,90,60,6.9,98,86,mid risk
|
| 82 |
+
16,90,65,6.9,98,76,mid risk
|
| 83 |
+
33,115,65,7,98,70,mid risk
|
| 84 |
+
12,95,60,6.9,98,65,mid risk
|
| 85 |
+
28,120,90,6.9,98,70,mid risk
|
| 86 |
+
21,90,65,6.9,98,76,mid risk
|
| 87 |
+
18,90,60,6.9,98,70,mid risk
|
| 88 |
+
21,120,80,6.9,98,76,mid risk
|
| 89 |
+
16,100,70,6.9,98,80,mid risk
|
| 90 |
+
19,120,75,6.9,98,66,mid risk
|
| 91 |
+
23,100,85,6.9,98,66,mid risk
|
| 92 |
+
22,120,90,7.8,98,82,mid risk
|
| 93 |
+
60,120,85,15,98,60,mid risk
|
| 94 |
+
13,90,65,7.8,101,80,mid risk
|
| 95 |
+
23,120,90,7.8,98,60,mid risk
|
| 96 |
+
28,115,60,7.8,101,86,mid risk
|
| 97 |
+
50,120,80,7.8,98,70,mid risk
|
| 98 |
+
29,130,70,7.8,98,78,mid risk
|
| 99 |
+
19,120,80,7,98,70,mid risk
|
| 100 |
+
19,120,85,7.8,98,60,mid risk
|
| 101 |
+
60,90,65,6.8,98,77,mid risk
|
| 102 |
+
55,120,90,6.8,98,66,mid risk
|
| 103 |
+
25,120,80,6.8,98,66,mid risk
|
| 104 |
+
48,140,90,15,98,90,high risk
|
| 105 |
+
25,140,100,6.8,98,80,high risk
|
| 106 |
+
23,140,90,6.8,98,70,high risk
|
| 107 |
+
34,85,60,11,102,86,high risk
|
| 108 |
+
50,140,90,15,98,90,high risk
|
| 109 |
+
25,140,100,6.8,98,80,high risk
|
| 110 |
+
42,140,100,18,98,90,high risk
|
| 111 |
+
32,140,100,7.9,98,78,high risk
|
| 112 |
+
50,140,95,17,98,60,high risk
|
| 113 |
+
38,135,60,7.9,101,86,high risk
|
| 114 |
+
39,90,70,9,98,80,high risk
|
| 115 |
+
30,140,100,15,98,70,high risk
|
| 116 |
+
63,140,90,15,98,90,high risk
|
| 117 |
+
25,140,100,7.9,98,80,high risk
|
| 118 |
+
30,120,80,7.9,101,76,high risk
|
| 119 |
+
55,140,100,18,98,90,high risk
|
| 120 |
+
32,140,100,7.9,98,78,high risk
|
| 121 |
+
30,140,100,15,98,70,high risk
|
| 122 |
+
48,120,80,11,98,88,high risk
|
| 123 |
+
49,140,90,15,98,90,high risk
|
| 124 |
+
25,140,100,7.5,98,80,high risk
|
| 125 |
+
40,160,100,19,98,77,high risk
|
| 126 |
+
32,140,90,18,98,88,high risk
|
| 127 |
+
35,140,100,7.5,98,66,high risk
|
| 128 |
+
54,140,100,15,98,66,high risk
|
| 129 |
+
55,140,95,19,98,77,high risk
|
| 130 |
+
29,120,70,9,98,80,high risk
|
| 131 |
+
48,120,80,11,98,88,high risk
|
| 132 |
+
40,160,100,19,98,77,high risk
|
| 133 |
+
32,140,90,18,98,88,high risk
|
| 134 |
+
35,140,100,7.5,98,66,high risk
|
| 135 |
+
54,140,100,15,98,66,high risk
|
| 136 |
+
40,120,95,11,98,80,high risk
|
| 137 |
+
22,90,60,7.5,102,60,high risk
|
| 138 |
+
40,120,85,15,98,60,high risk
|
| 139 |
+
55,140,95,19,98,77,high risk
|
| 140 |
+
50,130,100,16,98,75,high risk
|
| 141 |
+
18,120,80,6.9,102,76,mid risk
|
| 142 |
+
32,140,100,6.9,98,78,high risk
|
| 143 |
+
17,90,60,6.9,101,76,mid risk
|
| 144 |
+
17,90,63,6.9,101,70,mid risk
|
| 145 |
+
25,120,90,6.7,101,80,mid risk
|
| 146 |
+
17,120,80,6.7,102,76,mid risk
|
| 147 |
+
14,90,65,7,101,70,high risk
|
| 148 |
+
15,80,60,6.7,98,80,low risk
|
| 149 |
+
15,100,65,6.7,98,76,low risk
|
| 150 |
+
12,95,60,6.7,98,77,low risk
|
| 151 |
+
37,120,90,11,98,88,high risk
|
| 152 |
+
18,100,70,6.7,98,76,low risk
|
| 153 |
+
21,100,85,6.7,98,70,low risk
|
| 154 |
+
17,110,75,12,101,76,high risk
|
| 155 |
+
25,120,90,7.5,98,80,low risk
|
| 156 |
+
23,85,65,7.5,98,70,low risk
|
| 157 |
+
12,95,60,7.5,98,65,low risk
|
| 158 |
+
28,120,90,7.5,98,70,low risk
|
| 159 |
+
40,120,90,12,98,80,high risk
|
| 160 |
+
55,129,85,7.5,98,88,low risk
|
| 161 |
+
25,100,90,7.5,98,76,low risk
|
| 162 |
+
35,120,80,7.5,98,80,low risk
|
| 163 |
+
21,90,65,7.5,98,76,low risk
|
| 164 |
+
18,90,60,7.5,98,70,low risk
|
| 165 |
+
21,120,80,7.5,98,76,low risk
|
| 166 |
+
16,100,70,7.2,98,80,low risk
|
| 167 |
+
19,120,75,7.2,98,66,low risk
|
| 168 |
+
40,160,100,19,98,77,high risk
|
| 169 |
+
32,140,90,18,98,88,high risk
|
| 170 |
+
22,100,65,7.2,98,70,low risk
|
| 171 |
+
49,120,90,7.2,98,77,low risk
|
| 172 |
+
28,90,60,7.2,98,82,low risk
|
| 173 |
+
12,90,60,7.9,102,66,high risk
|
| 174 |
+
20,100,90,7.1,98,88,low risk
|
| 175 |
+
23,100,85,7.1,98,66,low risk
|
| 176 |
+
22,120,90,7.1,98,82,low risk
|
| 177 |
+
21,120,80,7.1,98,77,low risk
|
| 178 |
+
35,140,100,8,98,66,high risk
|
| 179 |
+
54,140,100,15,98,66,high risk
|
| 180 |
+
40,120,95,11,98,80,high risk
|
| 181 |
+
21,75,50,6.1,98,70,low risk
|
| 182 |
+
12,95,60,6.1,102,60,low risk
|
| 183 |
+
60,120,85,15,98,60,high risk
|
| 184 |
+
55,140,95,19,98,77,high risk
|
| 185 |
+
50,130,100,16,98,75,high risk
|
| 186 |
+
60,120,80,6.1,98,75,low risk
|
| 187 |
+
55,100,65,6.1,98,66,low risk
|
| 188 |
+
45,120,95,6.1,98,66,low risk
|
| 189 |
+
35,100,70,6.1,98,66,low risk
|
| 190 |
+
22,120,85,6.1,98,88,low risk
|
| 191 |
+
13,90,65,7.9,101,80,mid risk
|
| 192 |
+
23,120,90,6.1,98,60,low risk
|
| 193 |
+
17,90,65,6.1,103,67,high risk
|
| 194 |
+
28,83,60,8,101,86,high risk
|
| 195 |
+
50,120,80,15,98,70,high risk
|
| 196 |
+
25,90,70,6.1,98,80,low risk
|
| 197 |
+
30,120,80,6.1,98,70,low risk
|
| 198 |
+
31,120,60,6.1,98,76,mid risk
|
| 199 |
+
23,120,90,6.1,98,70,low risk
|
| 200 |
+
29,130,70,6.1,98,78,mid risk
|
| 201 |
+
17,85,60,9,102,86,high risk
|
| 202 |
+
32,120,90,7.5,98,70,low risk
|
| 203 |
+
42,120,80,7.5,98,70,low risk
|
| 204 |
+
23,90,60,7.5,98,76,low risk
|
| 205 |
+
19,120,80,7,98,70,mid risk
|
| 206 |
+
15,76,49,7.5,98,77,low risk
|
| 207 |
+
33,120,75,10,98,70,high risk
|
| 208 |
+
48,120,80,11,98,88,high risk
|
| 209 |
+
15,120,80,7,98,70,low risk
|
| 210 |
+
25,120,80,7,98,66,low risk
|
| 211 |
+
22,100,65,7,98,80,low risk
|
| 212 |
+
50,140,95,17,98,60,high risk
|
| 213 |
+
35,100,70,7,98,60,low risk
|
| 214 |
+
19,120,85,7,98,60,low risk
|
| 215 |
+
60,90,65,7,98,77,low risk
|
| 216 |
+
28,85,60,9,101,86,mid risk
|
| 217 |
+
50,140,80,6.7,98,70,mid risk
|
| 218 |
+
29,90,70,6.7,98,80,mid risk
|
| 219 |
+
30,140,100,15,98,70,high risk
|
| 220 |
+
31,120,60,6.1,98,76,mid risk
|
| 221 |
+
23,120,90,6.7,98,70,low risk
|
| 222 |
+
29,130,70,6.7,98,78,mid risk
|
| 223 |
+
17,85,60,9,102,86,mid risk
|
| 224 |
+
32,120,90,6.4,98,70,low risk
|
| 225 |
+
42,120,80,6.4,98,70,low risk
|
| 226 |
+
23,90,60,6.4,98,76,low risk
|
| 227 |
+
19,120,80,7,98,70,mid risk
|
| 228 |
+
15,76,49,6.4,98,77,low risk
|
| 229 |
+
29,120,75,7.2,100,70,high risk
|
| 230 |
+
48,120,80,11,98,88,high risk
|
| 231 |
+
15,120,80,7.2,98,70,low risk
|
| 232 |
+
50,140,90,15,98,77,high risk
|
| 233 |
+
25,140,100,7.2,98,80,high risk
|
| 234 |
+
55,140,80,7.2,101,76,high risk
|
| 235 |
+
20,110,60,7,100,70,mid risk
|
| 236 |
+
40,140,100,18,98,77,high risk
|
| 237 |
+
28,120,80,9,102,76,high risk
|
| 238 |
+
32,140,100,8,98,70,high risk
|
| 239 |
+
17,90,60,11,101,78,high risk
|
| 240 |
+
17,90,63,8,101,70,high risk
|
| 241 |
+
25,120,90,12,101,80,high risk
|
| 242 |
+
17,120,80,7,102,76,high risk
|
| 243 |
+
19,90,65,11,101,70,high risk
|
| 244 |
+
15,80,60,7,98,80,low risk
|
| 245 |
+
32,120,65,6,101,76,mid risk
|
| 246 |
+
12,95,60,7.2,98,77,low risk
|
| 247 |
+
37,120,90,11,98,88,high risk
|
| 248 |
+
18,100,70,6.8,98,76,low risk
|
| 249 |
+
21,100,85,6.9,98,70,low risk
|
| 250 |
+
17,110,75,13,101,76,high risk
|
| 251 |
+
25,120,90,15,98,80,high risk
|
| 252 |
+
10,85,65,6.9,98,70,low risk
|
| 253 |
+
12,95,60,6.9,98,65,low risk
|
| 254 |
+
28,120,90,6.9,98,70,low risk
|
| 255 |
+
40,120,90,6.9,98,80,low risk
|
| 256 |
+
55,110,85,6.9,98,88,low risk
|
| 257 |
+
25,100,90,6.9,98,76,low risk
|
| 258 |
+
35,120,80,6.9,98,80,low risk
|
| 259 |
+
21,90,65,6.9,98,76,low risk
|
| 260 |
+
18,90,60,6.9,98,70,low risk
|
| 261 |
+
21,120,80,6.9,98,76,low risk
|
| 262 |
+
16,100,70,6.9,98,80,low risk
|
| 263 |
+
19,120,75,6.9,98,66,low risk
|
| 264 |
+
40,160,100,19,98,77,high risk
|
| 265 |
+
32,140,90,18,98,88,high risk
|
| 266 |
+
22,100,65,6.9,98,70,low risk
|
| 267 |
+
49,120,90,6.9,98,77,low risk
|
| 268 |
+
28,90,60,6.9,98,82,low risk
|
| 269 |
+
12,90,60,8,102,66,high risk
|
| 270 |
+
20,100,90,7,98,88,low risk
|
| 271 |
+
23,100,85,7,98,66,low risk
|
| 272 |
+
22,120,90,7,98,82,low risk
|
| 273 |
+
21,120,80,7,98,77,low risk
|
| 274 |
+
35,140,100,9,98,66,high risk
|
| 275 |
+
54,140,100,15,98,66,high risk
|
| 276 |
+
40,120,95,11,98,80,high risk
|
| 277 |
+
21,75,50,7.7,98,60,low risk
|
| 278 |
+
12,90,60,11,102,60,high risk
|
| 279 |
+
60,120,85,15,98,60,high risk
|
| 280 |
+
55,140,95,19,98,77,high risk
|
| 281 |
+
50,130,100,16,98,76,high risk
|
| 282 |
+
60,120,80,7.7,98,75,low risk
|
| 283 |
+
55,100,65,7.7,98,66,low risk
|
| 284 |
+
45,120,95,7.7,98,66,low risk
|
| 285 |
+
35,100,70,7.7,98,66,low risk
|
| 286 |
+
22,120,85,7.7,98,88,low risk
|
| 287 |
+
13,90,65,9,101,80,high risk
|
| 288 |
+
23,120,90,7.7,98,60,low risk
|
| 289 |
+
17,90,65,7.7,103,67,high risk
|
| 290 |
+
26,85,60,6,101,86,mid risk
|
| 291 |
+
50,120,80,7.7,98,70,low risk
|
| 292 |
+
19,90,70,7.7,98,80,low risk
|
| 293 |
+
30,120,80,7.7,98,70,low risk
|
| 294 |
+
31,120,60,6.1,98,76,low risk
|
| 295 |
+
23,120,80,7.7,98,70,low risk
|
| 296 |
+
29,130,70,7.7,98,78,mid risk
|
| 297 |
+
17,85,60,6.3,102,86,high risk
|
| 298 |
+
32,120,90,7.7,98,70,low risk
|
| 299 |
+
42,120,80,7.7,98,70,low risk
|
| 300 |
+
23,90,60,7.7,98,76,low risk
|
| 301 |
+
19,120,80,7,98,70,mid risk
|
| 302 |
+
15,75,49,7.7,98,77,low risk
|
| 303 |
+
40,120,75,7.7,98,70,high risk
|
| 304 |
+
48,120,80,11,98,88,high risk
|
| 305 |
+
15,120,80,7.7,98,70,low risk
|
| 306 |
+
25,120,80,7.7,98,66,low risk
|
| 307 |
+
22,100,65,6.9,98,80,low risk
|
| 308 |
+
12,120,95,6.9,98,60,low risk
|
| 309 |
+
35,100,70,6.9,98,60,low risk
|
| 310 |
+
19,120,85,6.9,98,60,low risk
|
| 311 |
+
60,90,65,6.9,98,77,low risk
|
| 312 |
+
55,120,90,6.9,98,76,low risk
|
| 313 |
+
35,90,65,6.9,98,75,low risk
|
| 314 |
+
51,85,60,6.9,98,66,low risk
|
| 315 |
+
62,120,80,6.9,98,66,low risk
|
| 316 |
+
25,90,70,6.9,98,66,low risk
|
| 317 |
+
21,120,80,6.9,98,88,low risk
|
| 318 |
+
22,120,60,15,98,80,high risk
|
| 319 |
+
55,120,90,18,98,60,high risk
|
| 320 |
+
54,130,70,12,98,67,mid risk
|
| 321 |
+
35,85,60,19,98,86,high risk
|
| 322 |
+
43,120,90,18,98,70,high risk
|
| 323 |
+
12,120,80,6.9,98,80,low risk
|
| 324 |
+
65,90,60,6.9,98,70,low risk
|
| 325 |
+
60,120,80,6.9,98,76,low risk
|
| 326 |
+
25,120,90,6.9,98,70,low risk
|
| 327 |
+
22,90,65,6.9,98,78,low risk
|
| 328 |
+
66,85,60,6.9,98,86,low risk
|
| 329 |
+
56,120,80,13,98,70,high risk
|
| 330 |
+
35,90,70,6.9,98,70,low risk
|
| 331 |
+
43,120,80,15,98,76,high risk
|
| 332 |
+
35,120,60,6.9,98,70,low risk
|
| 333 |
+
44,120,90,16,98,80,mid risk
|
| 334 |
+
23,130,70,6.9,98,70,mid risk
|
| 335 |
+
22,85,60,6.9,98,76,mid risk
|
| 336 |
+
55,120,90,12,98,70,mid risk
|
| 337 |
+
35,120,80,6.9,98,78,mid risk
|
| 338 |
+
21,90,60,6.9,98,86,mid risk
|
| 339 |
+
45,120,80,6.9,103,70,low risk
|
| 340 |
+
70,85,60,6.9,102,70,low risk
|
| 341 |
+
65,120,90,6.9,103,76,low risk
|
| 342 |
+
55,120,80,6.9,102,80,low risk
|
| 343 |
+
45,90,60,18,101,70,high risk
|
| 344 |
+
22,120,80,6.9,103,76,low risk
|
| 345 |
+
16,90,65,6.9,98,76,mid risk
|
| 346 |
+
12,95,60,6.9,98,77,low risk
|
| 347 |
+
37,120,90,11,98,88,high risk
|
| 348 |
+
18,100,70,6.9,98,76,low risk
|
| 349 |
+
21,100,85,6.9,98,70,low risk
|
| 350 |
+
17,110,75,6.9,101,76,high risk
|
| 351 |
+
25,120,90,6.9,98,80,low risk
|
| 352 |
+
33,115,65,7,98,70,mid risk
|
| 353 |
+
12,95,60,6.9,98,65,mid risk
|
| 354 |
+
28,120,90,6.9,98,70,mid risk
|
| 355 |
+
40,120,90,6.9,98,80,high risk
|
| 356 |
+
55,110,85,6.9,98,88,high risk
|
| 357 |
+
25,100,90,6.9,98,76,high risk
|
| 358 |
+
35,120,80,6.9,98,80,high risk
|
| 359 |
+
21,90,65,6.9,98,76,mid risk
|
| 360 |
+
18,90,60,6.9,98,70,mid risk
|
| 361 |
+
21,120,80,6.9,98,76,mid risk
|
| 362 |
+
16,100,70,6.9,98,80,mid risk
|
| 363 |
+
19,120,75,6.9,98,66,mid risk
|
| 364 |
+
40,160,100,19,98,77,high risk
|
| 365 |
+
32,140,90,18,98,88,high risk
|
| 366 |
+
23,100,85,6.9,98,66,mid risk
|
| 367 |
+
22,120,90,7.8,98,82,mid risk
|
| 368 |
+
21,120,80,7.8,98,77,low risk
|
| 369 |
+
35,140,100,7.8,98,66,high risk
|
| 370 |
+
54,140,100,15,98,66,high risk
|
| 371 |
+
40,120,95,11,98,80,high risk
|
| 372 |
+
21,75,50,7.8,98,60,low risk
|
| 373 |
+
12,90,60,7.8,102,60,high risk
|
| 374 |
+
60,120,85,15,98,60,mid risk
|
| 375 |
+
55,140,95,19,98,77,high risk
|
| 376 |
+
50,130,100,16,98,75,high risk
|
| 377 |
+
60,120,80,7.8,98,75,high risk
|
| 378 |
+
55,100,65,7.8,98,66,low risk
|
| 379 |
+
45,120,95,7.8,98,66,low risk
|
| 380 |
+
35,100,70,7.8,98,66,low risk
|
| 381 |
+
22,120,85,7.8,98,88,low risk
|
| 382 |
+
13,90,65,7.8,101,80,mid risk
|
| 383 |
+
23,120,90,7.8,98,60,mid risk
|
| 384 |
+
17,90,65,7.8,103,67,high risk
|
| 385 |
+
28,115,60,7.8,101,86,mid risk
|
| 386 |
+
50,120,80,7.8,98,70,mid risk
|
| 387 |
+
19,90,70,7.8,98,80,low risk
|
| 388 |
+
30,120,80,7.8,98,70,low risk
|
| 389 |
+
31,120,60,6.1,98,76,low risk
|
| 390 |
+
23,120,70,7.8,98,70,low risk
|
| 391 |
+
29,130,70,7.8,98,78,mid risk
|
| 392 |
+
17,85,69,7.8,102,86,high risk
|
| 393 |
+
32,120,90,7.8,98,70,low risk
|
| 394 |
+
42,120,80,7.8,98,70,low risk
|
| 395 |
+
23,90,60,7.8,98,76,low risk
|
| 396 |
+
19,120,80,7,98,70,mid risk
|
| 397 |
+
15,76,49,7.8,98,77,low risk
|
| 398 |
+
20,120,75,7.8,98,70,low risk
|
| 399 |
+
48,120,80,11,98,88,high risk
|
| 400 |
+
15,120,80,7.8,98,70,low risk
|
| 401 |
+
25,120,80,7.8,98,66,low risk
|
| 402 |
+
22,100,65,7.8,98,80,low risk
|
| 403 |
+
12,120,95,7.8,98,60,low risk
|
| 404 |
+
35,100,70,7.8,98,60,low risk
|
| 405 |
+
19,120,85,7.8,98,60,mid risk
|
| 406 |
+
60,90,65,6.8,98,77,mid risk
|
| 407 |
+
55,120,90,6.8,98,66,mid risk
|
| 408 |
+
25,120,80,6.8,98,66,mid risk
|
| 409 |
+
22,100,65,6.8,98,88,low risk
|
| 410 |
+
12,120,95,6.8,98,60,mid risk
|
| 411 |
+
35,100,70,6.8,98,60,mid risk
|
| 412 |
+
19,120,90,6.8,98,60,mid risk
|
| 413 |
+
60,90,65,6.8,98,77,mid risk
|
| 414 |
+
55,120,90,6.8,98,78,low risk
|
| 415 |
+
50,130,80,16,102,76,mid risk
|
| 416 |
+
27,120,90,6.8,102,68,mid risk
|
| 417 |
+
60,140,90,12,98,77,high risk
|
| 418 |
+
55,100,70,6.8,101,80,mid risk
|
| 419 |
+
60,140,80,16,98,66,high risk
|
| 420 |
+
12,120,90,6.8,98,80,mid risk
|
| 421 |
+
17,140,100,6.8,103,80,high risk
|
| 422 |
+
60,120,80,6.8,98,77,mid risk
|
| 423 |
+
22,100,65,6.8,98,88,low risk
|
| 424 |
+
36,140,100,6.8,102,76,high risk
|
| 425 |
+
22,90,60,6.8,98,77,low risk
|
| 426 |
+
25,120,100,6.8,98,60,mid risk
|
| 427 |
+
35,100,60,15,98,80,high risk
|
| 428 |
+
40,140,100,13,101,66,high risk
|
| 429 |
+
27,120,70,6.8,98,77,low risk
|
| 430 |
+
36,140,100,6.8,102,76,high risk
|
| 431 |
+
22,90,60,6.8,98,77,mid risk
|
| 432 |
+
25,120,100,6.8,98,60,low risk
|
| 433 |
+
35,100,60,15,98,80,high risk
|
| 434 |
+
40,140,100,13,101,66,high risk
|
| 435 |
+
27,120,70,6.8,98,77,low risk
|
| 436 |
+
27,120,70,6.8,98,77,low risk
|
| 437 |
+
65,130,80,15,98,86,high risk
|
| 438 |
+
35,140,80,13,98,70,high risk
|
| 439 |
+
29,90,70,10,98,80,high risk
|
| 440 |
+
30,120,80,6.8,98,70,mid risk
|
| 441 |
+
35,120,60,6.1,98,76,mid risk
|
| 442 |
+
23,140,90,6.8,98,70,high risk
|
| 443 |
+
23,130,70,6.8,98,78,mid risk
|
| 444 |
+
35,85,60,11,102,86,high risk
|
| 445 |
+
32,120,90,6.8,98,70,low risk
|
| 446 |
+
43,130,80,18,98,70,mid risk
|
| 447 |
+
23,99,60,6.8,98,76,low risk
|
| 448 |
+
19,120,80,7,98,70,mid risk
|
| 449 |
+
15,76,49,6.8,98,77,low risk
|
| 450 |
+
30,120,75,6.8,98,70,mid risk
|
| 451 |
+
48,120,80,11,98,88,high risk
|
| 452 |
+
15,120,80,6.8,98,70,low risk
|
| 453 |
+
48,140,90,15,98,90,high risk
|
| 454 |
+
25,140,100,6.8,98,80,high risk
|
| 455 |
+
29,100,70,6.8,98,80,low risk
|
| 456 |
+
32,120,80,6.8,98,70,mid risk
|
| 457 |
+
35,120,60,6.1,98,76,low risk
|
| 458 |
+
23,140,90,6.8,98,70,high risk
|
| 459 |
+
23,130,70,6.8,98,78,mid risk
|
| 460 |
+
34,85,60,11,102,86,high risk
|
| 461 |
+
32,120,90,6.8,98,70,low risk
|
| 462 |
+
42,130,80,18,98,70,mid risk
|
| 463 |
+
23,90,60,6.8,98,76,low risk
|
| 464 |
+
19,120,80,7,98,70,mid risk
|
| 465 |
+
15,76,49,6.8,98,77,low risk
|
| 466 |
+
20,120,75,6.8,98,70,low risk
|
| 467 |
+
48,120,80,11,98,88,low risk
|
| 468 |
+
15,120,80,6.8,98,70,low risk
|
| 469 |
+
50,140,90,15,98,90,high risk
|
| 470 |
+
25,140,100,6.8,98,80,high risk
|
| 471 |
+
30,120,80,6.8,101,76,low risk
|
| 472 |
+
31,110,90,6.8,100,70,mid risk
|
| 473 |
+
42,140,100,18,98,90,high risk
|
| 474 |
+
18,120,80,6.8,102,76,low risk
|
| 475 |
+
32,140,100,7.9,98,78,high risk
|
| 476 |
+
17,90,60,7.9,101,76,low risk
|
| 477 |
+
19,120,80,7,98,70,mid risk
|
| 478 |
+
15,76,49,7.9,98,77,low risk
|
| 479 |
+
19,120,75,7.9,98,70,low risk
|
| 480 |
+
48,120,80,11,98,88,low risk
|
| 481 |
+
15,120,80,7.9,98,70,low risk
|
| 482 |
+
25,120,80,7.9,98,66,mid risk
|
| 483 |
+
22,100,65,7.9,98,80,low risk
|
| 484 |
+
50,140,95,17,98,60,high risk
|
| 485 |
+
35,100,70,7.9,98,60,low risk
|
| 486 |
+
19,120,85,7.9,98,60,low risk
|
| 487 |
+
60,90,65,7.9,98,77,low risk
|
| 488 |
+
38,135,60,7.9,101,86,high risk
|
| 489 |
+
50,120,80,7.9,98,70,low risk
|
| 490 |
+
39,90,70,9,98,80,high risk
|
| 491 |
+
30,140,100,15,98,70,high risk
|
| 492 |
+
31,120,60,6.1,98,76,mid risk
|
| 493 |
+
23,120,90,7.9,98,70,mid risk
|
| 494 |
+
29,130,70,7.9,98,78,mid risk
|
| 495 |
+
17,85,60,7.9,102,86,low risk
|
| 496 |
+
32,120,90,7.9,98,70,low risk
|
| 497 |
+
42,120,80,7.9,98,70,low risk
|
| 498 |
+
23,90,60,7.9,98,76,low risk
|
| 499 |
+
19,120,80,7,98,70,low risk
|
| 500 |
+
15,76,49,7.9,98,77,low risk
|
| 501 |
+
16,120,75,7.9,98,7,low risk
|
| 502 |
+
48,120,80,11,98,88,mid risk
|
| 503 |
+
15,120,80,7.9,98,70,low risk
|
| 504 |
+
63,140,90,15,98,90,high risk
|
| 505 |
+
25,140,100,7.9,98,80,high risk
|
| 506 |
+
30,120,80,7.9,101,76,high risk
|
| 507 |
+
17,70,50,7.9,98,70,low risk
|
| 508 |
+
55,140,100,18,98,90,high risk
|
| 509 |
+
18,120,80,7.9,102,76,mid risk
|
| 510 |
+
32,140,100,7.9,98,78,high risk
|
| 511 |
+
17,90,60,7.5,101,76,low risk
|
| 512 |
+
17,90,63,7.5,101,70,low risk
|
| 513 |
+
25,120,90,7.5,101,80,low risk
|
| 514 |
+
17,120,80,7.5,102,76,low risk
|
| 515 |
+
19,90,65,7.5,101,70,low risk
|
| 516 |
+
15,80,60,7.5,98,80,low risk
|
| 517 |
+
60,90,65,7.5,98,77,low risk
|
| 518 |
+
18,85,60,7.5,101,86,mid risk
|
| 519 |
+
50,120,80,7.5,98,70,low risk
|
| 520 |
+
19,90,70,7.5,98,80,low risk
|
| 521 |
+
30,140,100,15,98,70,high risk
|
| 522 |
+
31,120,60,6.1,98,76,low risk
|
| 523 |
+
23,120,90,7.5,98,70,low risk
|
| 524 |
+
29,130,70,7.5,98,78,mid risk
|
| 525 |
+
17,85,60,7.5,102,86,low risk
|
| 526 |
+
32,120,90,7.5,98,70,low risk
|
| 527 |
+
42,120,80,7.5,98,70,low risk
|
| 528 |
+
42,90,60,7.5,98,76,low risk
|
| 529 |
+
19,120,80,7,98,70,low risk
|
| 530 |
+
15,78,49,7.5,98,77,low risk
|
| 531 |
+
23,120,75,8,98,70,mid risk
|
| 532 |
+
48,120,80,11,98,88,high risk
|
| 533 |
+
15,120,80,7.5,98,70,mid risk
|
| 534 |
+
49,140,90,15,98,90,high risk
|
| 535 |
+
25,140,100,7.5,98,80,high risk
|
| 536 |
+
30,120,80,7.5,101,76,mid risk
|
| 537 |
+
16,70,50,7.5,100,70,low risk
|
| 538 |
+
16,100,70,7.5,98,80,low risk
|
| 539 |
+
19,120,75,7.5,98,66,low risk
|
| 540 |
+
40,160,100,19,98,77,high risk
|
| 541 |
+
32,140,90,18,98,88,high risk
|
| 542 |
+
22,100,65,7.5,98,70,low risk
|
| 543 |
+
49,120,90,7.5,98,77,low risk
|
| 544 |
+
28,90,60,7.5,98,82,low risk
|
| 545 |
+
12,90,60,7.5,102,66,low risk
|
| 546 |
+
20,100,90,7.5,98,88,low risk
|
| 547 |
+
23,100,85,7.5,98,66,low risk
|
| 548 |
+
22,120,90,7.5,98,82,low risk
|
| 549 |
+
21,120,80,7.5,98,77,low risk
|
| 550 |
+
35,140,100,7.5,98,66,high risk
|
| 551 |
+
54,140,100,15,98,66,high risk
|
| 552 |
+
40,120,95,11,98,80,mid risk
|
| 553 |
+
21,75,50,7.5,98,60,low risk
|
| 554 |
+
12,90,60,7.5,102,60,low risk
|
| 555 |
+
60,120,85,15,98,60,mid risk
|
| 556 |
+
55,140,95,19,98,77,high risk
|
| 557 |
+
50,130,100,16,98,75,mid risk
|
| 558 |
+
60,120,80,7.5,98,75,low risk
|
| 559 |
+
55,100,65,7.5,98,66,low risk
|
| 560 |
+
45,120,95,7.5,98,66,low risk
|
| 561 |
+
35,100,70,7.5,98,66,low risk
|
| 562 |
+
22,120,85,7.5,98,88,low risk
|
| 563 |
+
13,90,65,7.5,101,80,low risk
|
| 564 |
+
23,120,90,7.5,98,60,low risk
|
| 565 |
+
17,90,65,7.5,103,67,low risk
|
| 566 |
+
28,115,60,7.5,101,86,mid risk
|
| 567 |
+
59,120,80,7.5,98,70,low risk
|
| 568 |
+
29,120,70,9,98,80,high risk
|
| 569 |
+
23,120,80,7.5,98,70,low risk
|
| 570 |
+
31,120,60,6.1,98,76,mid risk
|
| 571 |
+
23,120,80,7.5,98,70,mid risk
|
| 572 |
+
29,130,70,7.5,98,78,mid risk
|
| 573 |
+
17,85,60,7.5,102,86,low risk
|
| 574 |
+
32,120,90,7.5,98,70,low risk
|
| 575 |
+
42,120,80,7.5,98,70,low risk
|
| 576 |
+
23,90,60,7.5,98,76,low risk
|
| 577 |
+
19,120,80,7,98,70,low risk
|
| 578 |
+
15,78,49,7.5,98,77,low risk
|
| 579 |
+
20,120,75,7.5,98,70,low risk
|
| 580 |
+
48,120,80,11,98,88,high risk
|
| 581 |
+
15,120,80,7.5,98,70,low risk
|
| 582 |
+
24,120,80,7.5,98,66,low risk
|
| 583 |
+
16,100,70,7.5,98,80,low risk
|
| 584 |
+
19,120,76,7.5,98,66,low risk
|
| 585 |
+
40,160,100,19,98,77,high risk
|
| 586 |
+
32,140,90,18,98,88,high risk
|
| 587 |
+
22,100,65,7.5,98,70,mid risk
|
| 588 |
+
49,120,90,7.5,98,77,mid risk
|
| 589 |
+
28,90,60,7.5,98,82,mid risk
|
| 590 |
+
12,90,60,7.5,102,66,mid risk
|
| 591 |
+
20,100,90,7.5,98,88,mid risk
|
| 592 |
+
23,100,85,7.5,98,66,mid risk
|
| 593 |
+
22,120,90,7.5,98,82,mid risk
|
| 594 |
+
21,120,80,7.5,98,77,mid risk
|
| 595 |
+
35,140,100,7.5,98,66,high risk
|
| 596 |
+
54,140,100,15,98,66,high risk
|
| 597 |
+
40,120,95,11,98,80,high risk
|
| 598 |
+
21,75,50,7.5,98,60,low risk
|
| 599 |
+
22,90,60,7.5,102,60,high risk
|
| 600 |
+
40,120,85,15,98,60,high risk
|
| 601 |
+
55,140,95,19,98,77,high risk
|
| 602 |
+
50,130,100,16,98,75,high risk
|
| 603 |
+
60,120,80,7.5,98,75,mid risk
|
| 604 |
+
40,120,85,15,98,60,high risk
|
| 605 |
+
55,140,95,19,98,77,high risk
|
| 606 |
+
50,130,100,16,98,75,mid risk
|
| 607 |
+
41,120,80,7.5,98,75,low risk
|
| 608 |
+
55,100,65,7.5,98,66,low risk
|
| 609 |
+
45,120,95,7.5,98,66,low risk
|
| 610 |
+
35,100,70,7.5,98,66,low risk
|
| 611 |
+
22,120,85,7.5,98,88,low risk
|
| 612 |
+
13,90,65,7.5,101,80,high risk
|
| 613 |
+
23,120,90,7.5,98,60,low risk
|
| 614 |
+
17,90,65,7.5,103,67,mid risk
|
| 615 |
+
27,135,60,7.5,101,86,high risk
|
| 616 |
+
50,120,80,15,98,70,high risk
|
| 617 |
+
34,110,70,7,98,80,high risk
|
| 618 |
+
32,120,80,7.5,98,70,low risk
|
| 619 |
+
31,120,60,6.1,98,76,low risk
|
| 620 |
+
23,120,90,7.5,98,70,low risk
|
| 621 |
+
29,130,70,7.5,98,78,mid risk
|
| 622 |
+
17,85,60,7.5,101,86,high risk
|
| 623 |
+
32,120,90,7.5,98,70,low risk
|
| 624 |
+
42,120,80,7.5,98,70,low risk
|
| 625 |
+
23,90,60,7.5,98,76,low risk
|
| 626 |
+
19,120,80,7,98,70,mid risk
|
| 627 |
+
15,76,49,7.5,98,77,low risk
|
| 628 |
+
20,120,76,7.5,98,70,low risk
|
| 629 |
+
48,120,80,11,98,88,high risk
|
| 630 |
+
15,120,80,7.5,98,70,low risk
|
| 631 |
+
24,120,80,7.5,98,66,low risk
|
| 632 |
+
22,100,65,12,98,80,high risk
|
| 633 |
+
50,140,95,17,98,60,high risk
|
| 634 |
+
35,100,70,11,98,60,high risk
|
| 635 |
+
19,120,85,9,98,60,mid risk
|
| 636 |
+
30,90,65,8,98,77,mid risk
|
| 637 |
+
28,85,60,9,101,86,mid risk
|
| 638 |
+
50,130,80,15,98,86,high risk
|
| 639 |
+
35,140,90,13,98,70,high risk
|
| 640 |
+
29,90,70,11,100,80,high risk
|
| 641 |
+
19,120,60,7,98.4,70,low risk
|
| 642 |
+
46,140,100,12,99,90,high risk
|
| 643 |
+
28,95,60,10,101,86,high risk
|
| 644 |
+
50,120,80,7,98,70,mid risk
|
| 645 |
+
39,110,70,7.9,98,80,mid risk
|
| 646 |
+
25,140,100,15,98.6,70,high risk
|
| 647 |
+
31,120,60,6.1,98,76,low risk
|
| 648 |
+
23,120,85,8,98,70,low risk
|
| 649 |
+
29,130,70,8,98,78,mid risk
|
| 650 |
+
17,90,60,9,102,86,mid risk
|
| 651 |
+
32,120,90,7,100,70,mid risk
|
| 652 |
+
42,120,90,9,98,70,mid risk
|
| 653 |
+
23,90,60,6.7,98,76,low risk
|
| 654 |
+
19,120,80,7,98,70,low risk
|
| 655 |
+
15,76,68,7,98,77,low risk
|
| 656 |
+
34,120,75,8,98,70,low risk
|
| 657 |
+
48,120,80,11,98,88,high risk
|
| 658 |
+
15,120,80,6.6,99,70,low risk
|
| 659 |
+
27,140,90,15,98,90,high risk
|
| 660 |
+
25,140,100,12,99,80,high risk
|
| 661 |
+
36,120,90,7,98,82,mid risk
|
| 662 |
+
30,120,80,9,101,76,mid risk
|
| 663 |
+
15,70,50,6,98,70,mid risk
|
| 664 |
+
40,120,95,7,98,70,high risk
|
| 665 |
+
15,90,60,6,98,80,low risk
|
| 666 |
+
21,90,50,6.9,98,60,low risk
|
| 667 |
+
15,90,49,6,98,77,low risk
|
| 668 |
+
21,90,50,6.5,98,60,low risk
|
| 669 |
+
15,90,49,6,98,77,low risk
|
| 670 |
+
15,90,49,6.7,99,77,low risk
|
| 671 |
+
15,90,49,6,99,77,low risk
|
| 672 |
+
10,100,50,6,99,70,mid risk
|
| 673 |
+
15,100,49,6.8,99,77,low risk
|
| 674 |
+
15,100,49,6,99,77,low risk
|
| 675 |
+
12,100,50,6.4,98,70,mid risk
|
| 676 |
+
15,100,60,6,98,80,low risk
|
| 677 |
+
35,140,90,13,98,70,high risk
|
| 678 |
+
29,90,70,8,100,80,high risk
|
| 679 |
+
30,140,85,7,98,70,high risk
|
| 680 |
+
23,140,80,7.01,98,70,high risk
|
| 681 |
+
35,85,60,11,102,86,high risk
|
| 682 |
+
42,130,80,18,98,70,high risk
|
| 683 |
+
50,140,90,15,98,90,high risk
|
| 684 |
+
25,140,100,7.01,98,80,high risk
|
| 685 |
+
40,140,100,18,98,90,high risk
|
| 686 |
+
32,140,100,6.9,98,78,high risk
|
| 687 |
+
14,90,65,7,101,70,high risk
|
| 688 |
+
37,120,90,11,98,88,high risk
|
| 689 |
+
17,110,75,12,101,76,high risk
|
| 690 |
+
40,120,90,12,98,80,high risk
|
| 691 |
+
40,160,100,19,98,77,high risk
|
| 692 |
+
20,120,76,7.5,98,70,low risk
|
| 693 |
+
15,120,80,7.5,98,70,low risk
|
| 694 |
+
24,120,80,7.5,98,66,low risk
|
| 695 |
+
19,120,60,7,98.4,70,low risk
|
| 696 |
+
31,120,60,6.1,98,76,low risk
|
| 697 |
+
23,120,85,8,98,70,low risk
|
| 698 |
+
23,90,60,6.7,98,76,low risk
|
| 699 |
+
19,120,80,7,98,70,low risk
|
| 700 |
+
15,76,68,7,98,77,low risk
|
| 701 |
+
34,120,75,8,98,70,low risk
|
| 702 |
+
15,120,80,6.6,99,70,low risk
|
| 703 |
+
15,90,60,6,98,80,low risk
|
| 704 |
+
21,90,50,6.9,98,60,low risk
|
| 705 |
+
15,100,49,7.6,98,77,low risk
|
| 706 |
+
12,100,50,6,98,70,mid risk
|
| 707 |
+
21,100,50,6.8,98,60,low risk
|
| 708 |
+
23,130,70,7.01,98,78,mid risk
|
| 709 |
+
32,120,90,6.9,98,70,mid risk
|
| 710 |
+
19,120,80,7,98,70,mid risk
|
| 711 |
+
20,120,75,7.01,100,70,mid risk
|
| 712 |
+
48,120,80,11,98,88,mid risk
|
| 713 |
+
30,120,80,6.9,101,76,mid risk
|
| 714 |
+
18,120,80,6.9,102,76,mid risk
|
| 715 |
+
17,90,60,6.9,101,76,mid risk
|
| 716 |
+
17,90,63,6.9,101,70,mid risk
|
| 717 |
+
25,120,90,6.7,101,80,mid risk
|
| 718 |
+
17,120,80,6.7,102,76,mid risk
|
| 719 |
+
13,90,65,7.9,101,80,mid risk
|
| 720 |
+
31,120,60,6.1,98,76,mid risk
|
| 721 |
+
29,130,70,6.1,98,78,mid risk
|
| 722 |
+
19,120,80,7,98,70,mid risk
|
| 723 |
+
28,85,60,9,101,86,mid risk
|
| 724 |
+
50,140,80,6.7,98,70,mid risk
|
| 725 |
+
29,90,70,6.7,98,80,mid risk
|
| 726 |
+
31,120,60,6.1,98,76,mid risk
|
| 727 |
+
29,130,70,6.7,98,78,mid risk
|
| 728 |
+
17,85,60,9,102,86,mid risk
|
| 729 |
+
19,120,80,7,98,70,mid risk
|
| 730 |
+
20,110,60,7,100,70,mid risk
|
| 731 |
+
19,120,80,7,98,70,mid risk
|
| 732 |
+
20,120,75,7.01,100,70,mid risk
|
| 733 |
+
48,120,80,11,98,88,mid risk
|
| 734 |
+
30,120,80,6.9,101,76,mid risk
|
| 735 |
+
18,120,80,6.9,102,76,mid risk
|
| 736 |
+
17,90,60,6.9,101,76,mid risk
|
| 737 |
+
17,90,63,6.9,101,70,mid risk
|
| 738 |
+
25,120,90,6.7,101,80,mid risk
|
| 739 |
+
17,120,80,6.7,102,76,mid risk
|
| 740 |
+
13,90,65,7.9,101,80,mid risk
|
| 741 |
+
31,120,60,6.1,98,76,mid risk
|
| 742 |
+
29,130,70,6.1,98,78,mid risk
|
| 743 |
+
19,120,80,7,98,70,mid risk
|
| 744 |
+
28,85,60,9,101,86,mid risk
|
| 745 |
+
50,140,80,6.7,98,70,mid risk
|
| 746 |
+
29,90,70,6.7,98,80,mid risk
|
| 747 |
+
31,120,60,6.1,98,76,mid risk
|
| 748 |
+
29,130,70,6.7,98,78,mid risk
|
| 749 |
+
17,85,60,9,102,86,mid risk
|
| 750 |
+
19,120,80,7,98,70,mid risk
|
| 751 |
+
20,110,60,7,100,70,mid risk
|
| 752 |
+
32,120,65,6,101,76,mid risk
|
| 753 |
+
26,85,60,6,101,86,mid risk
|
| 754 |
+
29,130,70,7.7,98,78,mid risk
|
| 755 |
+
19,120,80,7,98,70,mid risk
|
| 756 |
+
54,130,70,12,98,67,mid risk
|
| 757 |
+
44,120,90,16,98,80,mid risk
|
| 758 |
+
23,130,70,6.9,98,70,mid risk
|
| 759 |
+
22,85,60,6.9,98,76,mid risk
|
| 760 |
+
55,120,90,12,98,70,mid risk
|
| 761 |
+
35,120,80,6.9,98,78,mid risk
|
| 762 |
+
21,90,60,6.9,98,86,mid risk
|
| 763 |
+
16,90,65,6.9,98,76,mid risk
|
| 764 |
+
33,115,65,7,98,70,mid risk
|
| 765 |
+
12,95,60,6.9,98,65,mid risk
|
| 766 |
+
28,120,90,6.9,98,70,mid risk
|
| 767 |
+
21,90,65,6.9,98,76,mid risk
|
| 768 |
+
18,90,60,6.9,98,70,mid risk
|
| 769 |
+
21,120,80,6.9,98,76,mid risk
|
| 770 |
+
16,100,70,6.9,98,80,mid risk
|
| 771 |
+
19,120,75,6.9,98,66,mid risk
|
| 772 |
+
23,100,85,6.9,98,66,mid risk
|
| 773 |
+
22,120,90,7.8,98,82,mid risk
|
| 774 |
+
60,120,85,15,98,60,mid risk
|
| 775 |
+
13,90,65,7.8,101,80,mid risk
|
| 776 |
+
23,120,90,7.8,98,60,mid risk
|
| 777 |
+
28,115,60,7.8,101,86,mid risk
|
| 778 |
+
50,120,80,7.8,98,70,mid risk
|
| 779 |
+
29,130,70,7.8,98,78,mid risk
|
| 780 |
+
19,120,80,7,98,70,mid risk
|
| 781 |
+
19,120,85,7.8,98,60,mid risk
|
| 782 |
+
60,90,65,6.8,98,77,mid risk
|
| 783 |
+
55,120,90,6.8,98,66,mid risk
|
| 784 |
+
25,120,80,6.8,98,66,mid risk
|
| 785 |
+
12,120,95,6.8,98,60,mid risk
|
| 786 |
+
35,100,70,6.8,98,60,mid risk
|
| 787 |
+
19,120,90,6.8,98,60,mid risk
|
| 788 |
+
60,90,65,6.8,98,77,mid risk
|
| 789 |
+
50,130,80,16,102,76,mid risk
|
| 790 |
+
27,120,90,6.8,102,68,mid risk
|
| 791 |
+
55,100,70,6.8,101,80,mid risk
|
| 792 |
+
12,120,90,6.8,98,80,mid risk
|
| 793 |
+
60,120,80,6.8,98,77,mid risk
|
| 794 |
+
25,120,100,6.8,98,60,mid risk
|
| 795 |
+
22,90,60,6.8,98,77,mid risk
|
| 796 |
+
30,120,80,6.8,98,70,mid risk
|
| 797 |
+
35,120,60,6.1,98,76,mid risk
|
| 798 |
+
23,130,70,6.8,98,78,mid risk
|
| 799 |
+
43,130,80,18,98,70,mid risk
|
| 800 |
+
19,120,80,7,98,70,mid risk
|
| 801 |
+
30,120,75,6.8,98,70,mid risk
|
| 802 |
+
32,120,80,6.8,98,70,mid risk
|
| 803 |
+
23,130,70,6.8,98,78,mid risk
|
| 804 |
+
42,130,80,18,98,70,mid risk
|
| 805 |
+
19,120,80,7,98,70,mid risk
|
| 806 |
+
31,110,90,6.8,100,70,mid risk
|
| 807 |
+
19,120,80,7,98,70,mid risk
|
| 808 |
+
25,120,80,7.9,98,66,mid risk
|
| 809 |
+
31,120,60,6.1,98,76,mid risk
|
| 810 |
+
23,120,90,7.9,98,70,mid risk
|
| 811 |
+
29,130,70,7.9,98,78,mid risk
|
| 812 |
+
48,120,80,11,98,88,mid risk
|
| 813 |
+
18,120,80,7.9,102,76,mid risk
|
| 814 |
+
18,85,60,7.5,101,86,mid risk
|
| 815 |
+
29,130,70,7.5,98,78,mid risk
|
| 816 |
+
23,120,75,8,98,70,mid risk
|
| 817 |
+
15,120,80,7.5,98,70,mid risk
|
| 818 |
+
30,120,80,7.5,101,76,mid risk
|
| 819 |
+
40,120,95,11,98,80,mid risk
|
| 820 |
+
60,120,85,15,98,60,mid risk
|
| 821 |
+
50,130,100,16,98,75,mid risk
|
| 822 |
+
28,115,60,7.5,101,86,mid risk
|
| 823 |
+
31,120,60,6.1,98,76,mid risk
|
| 824 |
+
23,120,80,7.5,98,70,mid risk
|
| 825 |
+
29,130,70,7.5,98,78,mid risk
|
| 826 |
+
22,100,65,7.5,98,70,mid risk
|
| 827 |
+
49,120,90,7.5,98,77,mid risk
|
| 828 |
+
28,90,60,7.5,98,82,mid risk
|
| 829 |
+
12,90,60,7.5,102,66,mid risk
|
| 830 |
+
20,100,90,7.5,98,88,mid risk
|
| 831 |
+
23,100,85,7.5,98,66,mid risk
|
| 832 |
+
22,120,90,7.5,98,82,mid risk
|
| 833 |
+
21,120,80,7.5,98,77,mid risk
|
| 834 |
+
60,120,80,7.5,98,75,mid risk
|
| 835 |
+
50,130,100,16,98,75,mid risk
|
| 836 |
+
17,90,65,7.5,103,67,mid risk
|
| 837 |
+
29,130,70,7.5,98,78,mid risk
|
| 838 |
+
19,120,80,7,98,70,mid risk
|
| 839 |
+
19,120,85,9,98,60,mid risk
|
| 840 |
+
30,90,65,8,98,77,mid risk
|
| 841 |
+
28,85,60,9,101,86,mid risk
|
| 842 |
+
50,120,80,7,98,70,mid risk
|
| 843 |
+
39,110,70,7.9,98,80,mid risk
|
| 844 |
+
29,130,70,8,98,78,mid risk
|
| 845 |
+
17,90,60,9,102,86,mid risk
|
| 846 |
+
32,120,90,7,100,70,mid risk
|
| 847 |
+
42,120,90,9,98,70,mid risk
|
| 848 |
+
36,120,90,7,98,82,mid risk
|
| 849 |
+
30,120,80,9,101,76,mid risk
|
| 850 |
+
15,70,50,6,98,70,mid risk
|
| 851 |
+
10,100,50,6,99,70,mid risk
|
| 852 |
+
12,100,50,6.4,98,70,mid risk
|
| 853 |
+
12,100,50,6,98,70,mid risk
|
| 854 |
+
23,130,70,7.01,98,78,mid risk
|
| 855 |
+
32,120,90,6.9,98,70,mid risk
|
| 856 |
+
19,120,80,7,98,70,mid risk
|
| 857 |
+
20,120,75,7.01,100,70,mid risk
|
| 858 |
+
48,120,80,11,98,88,mid risk
|
| 859 |
+
30,120,80,6.9,101,76,mid risk
|
| 860 |
+
18,120,80,6.9,102,76,mid risk
|
| 861 |
+
17,90,60,6.9,101,76,mid risk
|
| 862 |
+
17,90,63,6.9,101,70,mid risk
|
| 863 |
+
25,120,90,6.7,101,80,mid risk
|
| 864 |
+
17,120,80,6.7,102,76,mid risk
|
| 865 |
+
13,90,65,7.9,101,80,mid risk
|
| 866 |
+
31,120,60,6.1,98,76,mid risk
|
| 867 |
+
29,130,70,6.1,98,78,mid risk
|
| 868 |
+
19,120,80,7,98,70,mid risk
|
| 869 |
+
28,85,60,9,101,86,mid risk
|
| 870 |
+
50,140,80,6.7,98,70,mid risk
|
| 871 |
+
29,90,70,6.7,98,80,mid risk
|
| 872 |
+
31,120,60,6.1,98,76,mid risk
|
| 873 |
+
29,130,70,6.7,98,78,mid risk
|
| 874 |
+
17,85,60,9,102,86,mid risk
|
| 875 |
+
19,120,80,7,98,70,mid risk
|
| 876 |
+
20,110,60,7,100,70,mid risk
|
| 877 |
+
32,120,65,6,101,76,mid risk
|
| 878 |
+
27,120,70,6.8,98,77,low risk
|
| 879 |
+
27,120,70,6.8,98,77,low risk
|
| 880 |
+
32,120,90,6.8,98,70,low risk
|
| 881 |
+
23,99,60,6.8,98,76,low risk
|
| 882 |
+
15,76,49,6.8,98,77,low risk
|
| 883 |
+
15,120,80,6.8,98,70,low risk
|
| 884 |
+
29,100,70,6.8,98,80,low risk
|
| 885 |
+
35,120,60,6.1,98,76,low risk
|
| 886 |
+
32,120,90,6.8,98,70,low risk
|
| 887 |
+
23,90,60,6.8,98,76,low risk
|
| 888 |
+
15,76,49,6.8,98,77,low risk
|
| 889 |
+
20,120,75,6.8,98,70,low risk
|
| 890 |
+
48,120,80,11,98,88,low risk
|
| 891 |
+
15,120,80,6.8,98,70,low risk
|
| 892 |
+
30,120,80,6.8,101,76,low risk
|
| 893 |
+
18,120,80,6.8,102,76,low risk
|
| 894 |
+
17,90,60,7.9,101,76,low risk
|
| 895 |
+
15,76,49,7.9,98,77,low risk
|
| 896 |
+
19,120,75,7.9,98,70,low risk
|
| 897 |
+
48,120,80,11,98,88,low risk
|
| 898 |
+
15,120,80,7.9,98,70,low risk
|
| 899 |
+
22,100,65,7.9,98,80,low risk
|
| 900 |
+
35,100,70,7.9,98,60,low risk
|
| 901 |
+
19,120,85,7.9,98,60,low risk
|
| 902 |
+
60,90,65,7.9,98,77,low risk
|
| 903 |
+
50,120,80,7.9,98,70,low risk
|
| 904 |
+
17,85,60,7.9,102,86,low risk
|
| 905 |
+
32,120,90,7.9,98,70,low risk
|
| 906 |
+
42,120,80,7.9,98,70,low risk
|
| 907 |
+
23,90,60,7.9,98,76,low risk
|
| 908 |
+
19,120,80,7,98,70,low risk
|
| 909 |
+
15,76,49,7.9,98,77,low risk
|
| 910 |
+
16,120,75,7.9,98,7,low risk
|
| 911 |
+
15,120,80,7.9,98,70,low risk
|
| 912 |
+
17,70,50,7.9,98,70,low risk
|
| 913 |
+
17,90,60,7.5,101,76,low risk
|
| 914 |
+
17,90,63,7.5,101,70,low risk
|
| 915 |
+
25,120,90,7.5,101,80,low risk
|
| 916 |
+
17,120,80,7.5,102,76,low risk
|
| 917 |
+
19,90,65,7.5,101,70,low risk
|
| 918 |
+
15,80,60,7.5,98,80,low risk
|
| 919 |
+
60,90,65,7.5,98,77,low risk
|
| 920 |
+
50,120,80,7.5,98,70,low risk
|
| 921 |
+
19,90,70,7.5,98,80,low risk
|
| 922 |
+
31,120,60,6.1,98,76,low risk
|
| 923 |
+
23,120,90,7.5,98,70,low risk
|
| 924 |
+
17,85,60,7.5,102,86,low risk
|
| 925 |
+
32,120,90,7.5,98,70,low risk
|
| 926 |
+
42,120,80,7.5,98,70,low risk
|
| 927 |
+
42,90,60,7.5,98,76,low risk
|
| 928 |
+
19,120,80,7,98,70,low risk
|
| 929 |
+
15,78,49,7.5,98,77,low risk
|
| 930 |
+
16,70,50,7.5,100,70,low risk
|
| 931 |
+
16,100,70,7.5,98,80,low risk
|
| 932 |
+
19,120,75,7.5,98,66,low risk
|
| 933 |
+
22,100,65,7.5,98,70,low risk
|
| 934 |
+
49,120,90,7.5,98,77,low risk
|
| 935 |
+
28,90,60,7.5,98,82,low risk
|
| 936 |
+
12,90,60,7.5,102,66,low risk
|
| 937 |
+
20,100,90,7.5,98,88,low risk
|
| 938 |
+
23,100,85,7.5,98,66,low risk
|
| 939 |
+
22,120,90,7.5,98,82,low risk
|
| 940 |
+
21,120,80,7.5,98,77,low risk
|
| 941 |
+
21,75,50,7.5,98,60,low risk
|
| 942 |
+
12,90,60,7.5,102,60,low risk
|
| 943 |
+
60,120,80,7.5,98,75,low risk
|
| 944 |
+
55,100,65,7.5,98,66,low risk
|
| 945 |
+
45,120,95,7.5,98,66,low risk
|
| 946 |
+
35,100,70,7.5,98,66,low risk
|
| 947 |
+
22,120,85,7.5,98,88,low risk
|
| 948 |
+
13,90,65,7.5,101,80,low risk
|
| 949 |
+
23,120,90,7.5,98,60,low risk
|
| 950 |
+
17,90,65,7.5,103,67,low risk
|
| 951 |
+
59,120,80,7.5,98,70,low risk
|
| 952 |
+
23,120,80,7.5,98,70,low risk
|
| 953 |
+
17,85,60,7.5,102,86,low risk
|
| 954 |
+
32,120,90,7.5,98,70,low risk
|
| 955 |
+
42,120,80,7.5,98,70,low risk
|
| 956 |
+
25,140,100,7.01,98,80,high risk
|
| 957 |
+
40,140,100,18,98,90,high risk
|
| 958 |
+
32,140,100,6.9,98,78,high risk
|
| 959 |
+
14,90,65,7,101,70,high risk
|
| 960 |
+
37,120,90,11,98,88,high risk
|
| 961 |
+
17,110,75,12,101,76,high risk
|
| 962 |
+
40,120,90,12,98,80,high risk
|
| 963 |
+
40,160,100,19,98,77,high risk
|
| 964 |
+
32,140,90,18,98,88,high risk
|
| 965 |
+
12,90,60,7.9,102,66,high risk
|
| 966 |
+
35,140,100,8,98,66,high risk
|
| 967 |
+
54,140,100,15,98,66,high risk
|
| 968 |
+
40,120,95,11,98,80,high risk
|
| 969 |
+
60,120,85,15,98,60,high risk
|
| 970 |
+
55,140,95,19,98,77,high risk
|
| 971 |
+
50,130,100,16,98,75,high risk
|
| 972 |
+
17,90,65,6.1,103,67,high risk
|
| 973 |
+
28,83,60,8,101,86,high risk
|
| 974 |
+
50,120,80,15,98,70,high risk
|
| 975 |
+
17,85,60,9,102,86,high risk
|
| 976 |
+
33,120,75,10,98,70,high risk
|
| 977 |
+
48,120,80,11,98,88,high risk
|
| 978 |
+
50,140,95,17,98,60,high risk
|
| 979 |
+
30,140,100,15,98,70,high risk
|
| 980 |
+
29,120,75,7.2,100,70,high risk
|
| 981 |
+
48,120,80,11,98,88,high risk
|
| 982 |
+
50,140,90,15,98,77,high risk
|
| 983 |
+
25,140,100,7.2,98,80,high risk
|
| 984 |
+
55,140,80,7.2,101,76,high risk
|
| 985 |
+
40,140,100,18,98,77,high risk
|
| 986 |
+
28,120,80,9,102,76,high risk
|
| 987 |
+
32,140,100,8,98,70,high risk
|
| 988 |
+
17,90,60,11,101,78,high risk
|
| 989 |
+
17,90,63,8,101,70,high risk
|
| 990 |
+
25,120,90,12,101,80,high risk
|
| 991 |
+
17,120,80,7,102,76,high risk
|
| 992 |
+
19,90,65,11,101,70,high risk
|
| 993 |
+
37,120,90,11,98,88,high risk
|
| 994 |
+
17,110,75,13,101,76,high risk
|
| 995 |
+
25,120,90,15,98,80,high risk
|
| 996 |
+
40,160,100,19,98,77,high risk
|
| 997 |
+
32,140,90,18,98,88,high risk
|
| 998 |
+
12,90,60,8,102,66,high risk
|
| 999 |
+
35,140,100,9,98,66,high risk
|
| 1000 |
+
54,140,100,15,98,66,high risk
|
| 1001 |
+
40,120,95,11,98,80,high risk
|
| 1002 |
+
12,90,60,11,102,60,high risk
|
| 1003 |
+
60,120,85,15,98,60,high risk
|
| 1004 |
+
55,140,95,19,98,77,high risk
|
| 1005 |
+
50,130,100,16,98,76,high risk
|
| 1006 |
+
13,90,65,9,101,80,high risk
|
| 1007 |
+
17,90,65,7.7,103,67,high risk
|
| 1008 |
+
17,85,60,6.3,102,86,high risk
|
| 1009 |
+
40,120,75,7.7,98,70,high risk
|
| 1010 |
+
48,120,80,11,98,88,high risk
|
| 1011 |
+
22,120,60,15,98,80,high risk
|
| 1012 |
+
55,120,90,18,98,60,high risk
|
| 1013 |
+
35,85,60,19,98,86,high risk
|
| 1014 |
+
43,120,90,18,98,70,high risk
|
| 1015 |
+
32,120,65,6,101,76,mid risk
|
deployment/ngos_guide.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying MamaGuard at your clinic or NGO
|
| 2 |
+
|
| 3 |
+
## What you need
|
| 4 |
+
|
| 5 |
+
- Any laptop or desktop computer (even a 2015 model works)
|
| 6 |
+
- Python 3.10 or higher (free download at python.org)
|
| 7 |
+
- The clinic's local WiFi network (so multiple devices can use the dashboard)
|
| 8 |
+
|
| 9 |
+
## Setup for a new clinic (one-time, ~30 minutes)
|
| 10 |
+
|
| 11 |
+
1. Copy the entire mamaGuard/ folder onto the clinic computer
|
| 12 |
+
2. Open a terminal (Command Prompt on Windows)
|
| 13 |
+
3. cd into the mamaGuard folder
|
| 14 |
+
4. Run: pip install -r requirements.txt
|
| 15 |
+
5. Run: python -m src.train
|
| 16 |
+
6. Run: uvicorn api.main:app --host 0.0.0.0 --port 8000
|
| 17 |
+
(--host 0.0.0.0 means any device on the network can reach it)
|
| 18 |
+
|
| 19 |
+
## Using the dashboard (for health workers)
|
| 20 |
+
|
| 21 |
+
1. On any phone or tablet connected to the clinic WiFi, open a browser
|
| 22 |
+
2. Go to: http://[clinic-computer-IP]:8000/dashboard
|
| 23 |
+
(The IT person can find the IP in the computer's network settings)
|
| 24 |
+
3. Enter the patient's ID and their prenatal visit readings
|
| 25 |
+
4. Press "Assess maternal risk"
|
| 26 |
+
5. Follow the action instruction shown
|
| 27 |
+
|
| 28 |
+
## Understanding the three alert levels
|
| 29 |
+
|
| 30 |
+
🟢 GREEN — Continue standard care. Schedule next visit as normal.
|
| 31 |
+
🟡 AMBER — Elevated risk. Add an extra checkup within 72 hours.
|
| 32 |
+
🔴 RED — High risk. Refer to hospital. If a transfer order is shown, call
|
| 33 |
+
the referral line immediately.
|
| 34 |
+
|
| 35 |
+
## What to do about LOW CONFIDENCE alerts
|
| 36 |
+
|
| 37 |
+
If you see "Data quality: low" on any result, record the missing readings
|
| 38 |
+
at the next visit. The model's accuracy improves significantly when all
|
| 39 |
+
six fields (Age, Systolic BP, Diastolic BP, Blood Sugar, Temperature,
|
| 40 |
+
Heart Rate) are filled in.
|
| 41 |
+
|
| 42 |
+
## Weekly reporting
|
| 43 |
+
|
| 44 |
+
Go to http://[clinic-computer-IP]:8000/stats to see a summary of all
|
| 45 |
+
patients assessed this week, broken down by risk level. This can be
|
| 46 |
+
emailed to district health authorities.
|
| 47 |
+
|
| 48 |
+
## If the server is not running
|
| 49 |
+
|
| 50 |
+
The health worker will see "Could not reach the server."
|
| 51 |
+
Ask the IT contact to re-run: uvicorn api.main:app --host 0.0.0.0 --port 8000
|
models/confusion_matrix.png
ADDED
|
models/evaluation_report.txt
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
╔══════════════════════════════════════════════════════════════════════════╗
|
| 3 |
+
║ MAMAGUARD — MODEL EVALUATION REPORT ║
|
| 4 |
+
║ Generated automatically by src/evaluate.py ║
|
| 5 |
+
╚══════════════════════════════════════════════════════════════════════════╝
|
| 6 |
+
|
| 7 |
+
MODEL
|
| 8 |
+
Architecture : MamaGuard-Mamba3 (Trapezoidal SSM + MIMO + Complex state)
|
| 9 |
+
Parameters : ~287,815
|
| 10 |
+
Input : Sequence of prenatal visits (up to 5) × 6 vital signs
|
| 11 |
+
Output : 3-class risk prediction (Low / Medium / High)
|
| 12 |
+
Dataset : UCI Maternal Health Risk (1,014 rows)
|
| 13 |
+
Training set : 1525 sequences
|
| 14 |
+
Validation set: 191 sequences
|
| 15 |
+
|
| 16 |
+
──────────────────────────────────────────────────────────────────────────
|
| 17 |
+
PERFORMANCE METRICS (on held-out validation set)
|
| 18 |
+
──────────────────────────────────────────────────────────────────────────
|
| 19 |
+
|
| 20 |
+
Overall accuracy : 0.880 (88.0%)
|
| 21 |
+
|
| 22 |
+
ROC-AUC per class:
|
| 23 |
+
Low risk : 0.977
|
| 24 |
+
Medium risk : 0.925
|
| 25 |
+
High risk : 0.995
|
| 26 |
+
|
| 27 |
+
** HIGH RISK RECALL (most critical for patient safety):
|
| 28 |
+
1.000 — of all truly high-risk patients, 100.0% were correctly flagged
|
| 29 |
+
|
| 30 |
+
** HIGH RISK PRECISION (alarm fatigue indicator):
|
| 31 |
+
0.895 — of all patients flagged high-risk, 89.5% truly were
|
| 32 |
+
|
| 33 |
+
Detailed per-class report:
|
| 34 |
+
precision recall f1-score support
|
| 35 |
+
|
| 36 |
+
Low risk 0.917 0.857 0.886 77
|
| 37 |
+
Medium risk 0.823 0.810 0.816 63
|
| 38 |
+
High risk 0.895 1.000 0.944 51
|
| 39 |
+
|
| 40 |
+
accuracy 0.880 191
|
| 41 |
+
macro avg 0.878 0.889 0.882 191
|
| 42 |
+
weighted avg 0.880 0.880 0.878 191
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
──────────────────────────────────────────────────────────────────────────
|
| 46 |
+
CONFUSION MATRIX (raw counts)
|
| 47 |
+
──────────────────────────────────────────────────────────────────────────
|
| 48 |
+
Rows = true label, Columns = predicted label
|
| 49 |
+
|
| 50 |
+
Pred: Low Pred: Mid Pred: High
|
| 51 |
+
True: Low 66 11 0
|
| 52 |
+
True: Mid 6 51 6
|
| 53 |
+
True: High 0 0 51
|
| 54 |
+
|
| 55 |
+
Most dangerous mistakes (False Negatives for High Risk):
|
| 56 |
+
High-risk patients predicted as Low risk : 0
|
| 57 |
+
High-risk patients predicted as Mid risk : 0
|
| 58 |
+
|
| 59 |
+
──────────────────────────────────────────────────────────────────────────
|
| 60 |
+
HYBRID SYSTEM NOTE
|
| 61 |
+
──────────────────────────────────────────────────────────────────────────
|
| 62 |
+
MamaGuard uses a HYBRID architecture:
|
| 63 |
+
1. Mamba3 model: handles subtle temporal patterns (learned from data)
|
| 64 |
+
2. WHO clinical rules: hard overrides for obvious danger signs
|
| 65 |
+
- Rule 1: SystolicBP >= 160 -> RED
|
| 66 |
+
- Rule 2: SystolicBP >= 140 -> AMBER minimum
|
| 67 |
+
- Rule 3: Blood sugar > 11.1 -> AMBER minimum
|
| 68 |
+
- Rule 4: BP rise >= 20 mmHg -> AMBER minimum
|
| 69 |
+
- Rule 5: 3+ vitals escalating simultaneously -> RED
|
| 70 |
+
|
| 71 |
+
The metrics above reflect the NEURAL MODEL ONLY (without clinical rules).
|
| 72 |
+
In deployment, the clinical rules provide an additional safety floor,
|
| 73 |
+
meaning real-world recall for high-risk cases is higher than shown above.
|
| 74 |
+
|
| 75 |
+
──────────────────────────────────────────────────────────────────────────
|
| 76 |
+
LIMITATIONS
|
| 77 |
+
──────────────────────────────────────────────────────────────────────────
|
| 78 |
+
1. SMALL DATASET: Trained on 1,014 rows from a single UCI dataset.
|
| 79 |
+
Real-world clinical models typically require 10,000–100,000+ samples.
|
| 80 |
+
|
| 81 |
+
2. SYNTHETIC SEQUENCES: The UCI dataset has no patient IDs or timestamps.
|
| 82 |
+
We created artificial 5-visit sequences by sorting rows by age.
|
| 83 |
+
These do not represent real patient trajectories.
|
| 84 |
+
|
| 85 |
+
3. NOT CLINICALLY VALIDATED: This model has NOT been validated against
|
| 86 |
+
real patient outcomes in a clinical setting. It must NOT be used
|
| 87 |
+
for actual medical decisions without proper clinical validation trials.
|
| 88 |
+
|
| 89 |
+
4. POPULATION BIAS: The UCI dataset was collected from a specific
|
| 90 |
+
population. Performance may differ on patients from different
|
| 91 |
+
regions, ethnicities, or healthcare contexts.
|
| 92 |
+
|
| 93 |
+
5. RESEARCH PROTOTYPE: This is a proof-of-concept demonstrating the
|
| 94 |
+
application of Mamba3 SSMs to maternal health risk prediction.
|
| 95 |
+
The system design (alarm fatigue mitigation, resource-aware routing,
|
| 96 |
+
OCR auto-fill) represents the primary contribution of this work.
|
| 97 |
+
|
| 98 |
+
──────────────────────────────────────────────────────────────────────────
|
| 99 |
+
CITATION
|
| 100 |
+
──────────────────────────────────────────────────────────────────────────
|
| 101 |
+
If you use this work, please cite:
|
| 102 |
+
MamaGuard: Maternal Mortality Early Warning using Mamba3 Sequential
|
| 103 |
+
State-Space Models with Clinical Safety Rules.
|
| 104 |
+
[Your Name], 2025. GitHub: [your-repo-url]
|
| 105 |
+
Based on: Gu & Dao (2023) Mamba; UCI Maternal Health Risk dataset.
|
| 106 |
+
|
| 107 |
+
══════════════════════════════════════════════════════════════════════════
|
models/mamaguard_mamba3.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ad79f8d1baea4ba994bba405e0e849cb5a19d0960ad4d2b19eb7a0cbec985873
|
| 3 |
+
size 1179269
|
models/roc_curves.png
ADDED
|
models/scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:90559a3874dfc35c38feea7d5b08faf2a42eb023ef6e2dc23e9c8e42e1a77cee
|
| 3 |
+
size 560
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.1.0
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
pandas>=2.0.0
|
| 4 |
+
scikit-learn>=1.3.0
|
| 5 |
+
fastapi>=0.104.0
|
| 6 |
+
uvicorn>=0.24.0
|
| 7 |
+
pydantic>=2.0.0
|
| 8 |
+
shap>=0.44.0
|
| 9 |
+
einops>=0.7.0
|
| 10 |
+
matplotlib>=3.7.0
|
| 11 |
+
opencv-python-headless>=4.8.0
|
| 12 |
+
pytesseract>=0.3.10
|
| 13 |
+
Pillow>=10.0.0
|
| 14 |
+
python-multipart>=0.0.6
|
src/__init__.py
ADDED
|
File without changes
|
src/data_pipeline.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — Data Pipeline
|
| 3 |
+
Loads UCI CSV, cleans, scales, builds sequences, and splits into train/val.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
from torch.utils.data import Dataset, DataLoader
|
| 10 |
+
from sklearn.preprocessing import StandardScaler
|
| 11 |
+
from sklearn.model_selection import train_test_split
|
| 12 |
+
|
| 13 |
+
# ─── Constants ────────────────────────────────────────────────────────────────
|
| 14 |
+
|
| 15 |
+
FEATURE_COLS = ['Age', 'SystolicBP', 'DiastolicBP', 'BS', 'BodyTemp', 'HeartRate']
|
| 16 |
+
LABEL_COL = 'RiskLevel'
|
| 17 |
+
LABEL_MAP = {'low risk': 0, 'mid risk': 1, 'high risk': 2}
|
| 18 |
+
SEQ_LEN = 5
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ─── Data quality scorer ─────────────────────────────────────────────────────
|
| 22 |
+
|
| 23 |
+
def compute_data_quality(row: pd.Series) -> float:
|
| 24 |
+
"""Returns 0.0–1.0 indicating how complete and plausible the data is."""
|
| 25 |
+
score, total = 0.0, 0
|
| 26 |
+
|
| 27 |
+
for col in FEATURE_COLS:
|
| 28 |
+
total += 1
|
| 29 |
+
if pd.notna(row.get(col)):
|
| 30 |
+
score += 1.0
|
| 31 |
+
|
| 32 |
+
plausibility = {
|
| 33 |
+
'Age': (10, 60), 'SystolicBP': (70, 200), 'DiastolicBP': (40, 130),
|
| 34 |
+
'BS': (3.0, 20.0), 'BodyTemp': (35.0, 42.0), 'HeartRate': (40, 160),
|
| 35 |
+
}
|
| 36 |
+
for col, (lo, hi) in plausibility.items():
|
| 37 |
+
total += 1
|
| 38 |
+
val = row.get(col)
|
| 39 |
+
if pd.notna(val) and lo <= val <= hi:
|
| 40 |
+
score += 1.0
|
| 41 |
+
|
| 42 |
+
return round(score / total, 3)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ─── Sequence builders ────────────────────────────────────────────────────────
|
| 46 |
+
|
| 47 |
+
def build_type_a_sequences(X: np.ndarray, y: np.ndarray,
|
| 48 |
+
quality: np.ndarray) -> tuple:
|
| 49 |
+
"""Type A — Within-risk-group sliding window sequences."""
|
| 50 |
+
X_seq, y_seq, q_seq = [], [], []
|
| 51 |
+
|
| 52 |
+
for risk_label in [0, 1, 2]:
|
| 53 |
+
idx = np.where(y == risk_label)[0]
|
| 54 |
+
if len(idx) < SEQ_LEN:
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
age_order = np.argsort(X[idx, 0])
|
| 58 |
+
idx_sorted = idx[age_order]
|
| 59 |
+
|
| 60 |
+
for i in range(len(idx_sorted) - SEQ_LEN + 1):
|
| 61 |
+
seq_idx = idx_sorted[i : i + SEQ_LEN]
|
| 62 |
+
X_seq.append(X[seq_idx].copy())
|
| 63 |
+
y_seq.append(risk_label)
|
| 64 |
+
q_seq.append(quality[seq_idx].mean())
|
| 65 |
+
|
| 66 |
+
return X_seq, y_seq, q_seq
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_type_b_sequences(X: np.ndarray, y: np.ndarray,
|
| 70 |
+
rng: np.random.Generator) -> tuple:
|
| 71 |
+
"""Type B — Synthetic escalation sequences for mid and high risk."""
|
| 72 |
+
X_seq, y_seq, q_seq = [], [], []
|
| 73 |
+
|
| 74 |
+
for risk_label in [1, 2]:
|
| 75 |
+
indices = np.where(y == risk_label)[0]
|
| 76 |
+
for idx in indices:
|
| 77 |
+
anchor = X[idx]
|
| 78 |
+
seq = []
|
| 79 |
+
for step in range(SEQ_LEN):
|
| 80 |
+
fraction = 0.30 + 0.70 * (step / (SEQ_LEN - 1))
|
| 81 |
+
noise = rng.normal(0, 0.05, anchor.shape)
|
| 82 |
+
seq.append(anchor * fraction + noise)
|
| 83 |
+
X_seq.append(np.array(seq, dtype=np.float32))
|
| 84 |
+
y_seq.append(risk_label)
|
| 85 |
+
q_seq.append(1.0)
|
| 86 |
+
|
| 87 |
+
return X_seq, y_seq, q_seq
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_type_c_sequences(X: np.ndarray, y: np.ndarray,
|
| 91 |
+
rng: np.random.Generator,
|
| 92 |
+
n_per_transition: int = 80) -> tuple:
|
| 93 |
+
"""Type C — Cross-risk deterioration sequences (low->mid, low->high, mid->high)."""
|
| 94 |
+
X_seq, y_seq, q_seq = [], [], []
|
| 95 |
+
|
| 96 |
+
low_idx = np.where(y == 0)[0]
|
| 97 |
+
mid_idx = np.where(y == 1)[0]
|
| 98 |
+
high_idx = np.where(y == 2)[0]
|
| 99 |
+
|
| 100 |
+
if len(low_idx) == 0 or len(mid_idx) == 0 or len(high_idx) == 0:
|
| 101 |
+
return X_seq, y_seq, q_seq
|
| 102 |
+
|
| 103 |
+
def blend(a: np.ndarray, b: np.ndarray, frac: float = 0.5) -> np.ndarray:
|
| 104 |
+
"""Linear interpolation between two rows + small noise."""
|
| 105 |
+
return a * (1 - frac) + b * frac + rng.normal(0, 0.05, a.shape)
|
| 106 |
+
|
| 107 |
+
def pick(indices: np.ndarray) -> np.ndarray:
|
| 108 |
+
"""Pick one random row from an index array."""
|
| 109 |
+
return X[rng.choice(indices)]
|
| 110 |
+
|
| 111 |
+
# Transition 1: LOW -> HIGH
|
| 112 |
+
for _ in range(n_per_transition):
|
| 113 |
+
seq = [
|
| 114 |
+
pick(low_idx), pick(low_idx),
|
| 115 |
+
blend(pick(low_idx), pick(high_idx)),
|
| 116 |
+
pick(high_idx), pick(high_idx),
|
| 117 |
+
]
|
| 118 |
+
X_seq.append(np.array(seq, dtype=np.float32))
|
| 119 |
+
y_seq.append(2)
|
| 120 |
+
q_seq.append(1.0)
|
| 121 |
+
|
| 122 |
+
# Transition 2: LOW -> MID
|
| 123 |
+
for _ in range(n_per_transition):
|
| 124 |
+
seq = [
|
| 125 |
+
pick(low_idx), pick(low_idx), pick(low_idx),
|
| 126 |
+
pick(mid_idx), pick(mid_idx),
|
| 127 |
+
]
|
| 128 |
+
X_seq.append(np.array(seq, dtype=np.float32))
|
| 129 |
+
y_seq.append(1)
|
| 130 |
+
q_seq.append(1.0)
|
| 131 |
+
|
| 132 |
+
# Transition 3: MID -> HIGH
|
| 133 |
+
for _ in range(n_per_transition):
|
| 134 |
+
seq = [
|
| 135 |
+
pick(mid_idx), pick(mid_idx),
|
| 136 |
+
blend(pick(mid_idx), pick(high_idx)),
|
| 137 |
+
pick(high_idx), pick(high_idx),
|
| 138 |
+
]
|
| 139 |
+
X_seq.append(np.array(seq, dtype=np.float32))
|
| 140 |
+
y_seq.append(2)
|
| 141 |
+
q_seq.append(1.0)
|
| 142 |
+
|
| 143 |
+
return X_seq, y_seq, q_seq
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# ─── Internal helper ─────────────────────────────────────────────────────────
|
| 147 |
+
|
| 148 |
+
def _build_all_sequences(X: np.ndarray, y: np.ndarray,
|
| 149 |
+
quality: np.ndarray,
|
| 150 |
+
rng: np.random.Generator,
|
| 151 |
+
include_augmentation: bool,
|
| 152 |
+
label: str = "") -> tuple:
|
| 153 |
+
"""Builds all sequence types from a given set of scaled rows."""
|
| 154 |
+
X_all, y_all, q_all = [], [], []
|
| 155 |
+
|
| 156 |
+
a_X, a_y, a_q = build_type_a_sequences(X, y, quality)
|
| 157 |
+
X_all += a_X; y_all += a_y; q_all += a_q
|
| 158 |
+
print(f" {label} Type A (within-class): {len(a_X):5d} sequences")
|
| 159 |
+
|
| 160 |
+
if include_augmentation:
|
| 161 |
+
b_X, b_y, b_q = build_type_b_sequences(X, y, rng)
|
| 162 |
+
X_all += b_X; y_all += b_y; q_all += b_q
|
| 163 |
+
print(f" {label} Type B (escalation): {len(b_X):5d} sequences")
|
| 164 |
+
|
| 165 |
+
min_cls = min(sum(y == 0), sum(y == 1), sum(y == 2))
|
| 166 |
+
if min_cls >= SEQ_LEN:
|
| 167 |
+
c_X, c_y, c_q = build_type_c_sequences(X, y, rng)
|
| 168 |
+
X_all += c_X; y_all += c_y; q_all += c_q
|
| 169 |
+
print(f" {label} Type C (cross-risk): {len(c_X):5d} sequences")
|
| 170 |
+
else:
|
| 171 |
+
print(f" {label} Type C skipped (smallest class has {min_cls} rows)")
|
| 172 |
+
|
| 173 |
+
return X_all, y_all, q_all
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ─── Main pipeline ────────────────────────────────────────────────────────────
|
| 177 |
+
|
| 178 |
+
def load_and_preprocess(csv_path: str):
|
| 179 |
+
"""
|
| 180 |
+
Returns pre-split, pre-scaled sequence arrays + the fitted scaler.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
X_train, y_train, q_train, X_val, y_val, q_val, scaler
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
# 1. Load
|
| 187 |
+
df = pd.read_csv(csv_path)
|
| 188 |
+
print(f"Loaded {len(df)} rows from {csv_path}")
|
| 189 |
+
df[LABEL_COL] = df[LABEL_COL].str.lower().str.strip()
|
| 190 |
+
|
| 191 |
+
# 2. Quality scores BEFORE imputation
|
| 192 |
+
quality_per_row = df.apply(compute_data_quality, axis=1).values
|
| 193 |
+
|
| 194 |
+
# 3. Impute missing values
|
| 195 |
+
for col in FEATURE_COLS:
|
| 196 |
+
df[col] = df[col].fillna(df[col].median())
|
| 197 |
+
|
| 198 |
+
X_raw = df[FEATURE_COLS].values.astype(np.float32)
|
| 199 |
+
y_raw = df[LABEL_COL].map(LABEL_MAP).values
|
| 200 |
+
|
| 201 |
+
# 4. Split raw rows first (prevents data leakage and sequence overlap)
|
| 202 |
+
(X_train_raw, X_val_raw,
|
| 203 |
+
y_train_raw, y_val_raw,
|
| 204 |
+
q_train_raw, q_val_raw) = train_test_split(
|
| 205 |
+
X_raw, y_raw, quality_per_row,
|
| 206 |
+
test_size=0.2, random_state=42, stratify=y_raw
|
| 207 |
+
)
|
| 208 |
+
print(f"Raw split -> train: {len(X_train_raw)} rows | val: {len(X_val_raw)} rows")
|
| 209 |
+
print(f"Train classes: low={sum(y_train_raw==0)} "
|
| 210 |
+
f"mid={sum(y_train_raw==1)} high={sum(y_train_raw==2)}")
|
| 211 |
+
print(f"Val classes: low={sum(y_val_raw==0)} "
|
| 212 |
+
f"mid={sum(y_val_raw==1)} high={sum(y_val_raw==2)}")
|
| 213 |
+
|
| 214 |
+
# 5. Fit scaler on train rows only
|
| 215 |
+
scaler = StandardScaler()
|
| 216 |
+
X_train_scaled = scaler.fit_transform(X_train_raw)
|
| 217 |
+
X_val_scaled = scaler.transform(X_val_raw)
|
| 218 |
+
|
| 219 |
+
# 6. Build sequences separately
|
| 220 |
+
rng = np.random.default_rng(42)
|
| 221 |
+
|
| 222 |
+
print("\nBuilding TRAINING sequences:")
|
| 223 |
+
train_seqs = _build_all_sequences(
|
| 224 |
+
X_train_scaled, y_train_raw, q_train_raw,
|
| 225 |
+
rng, include_augmentation=True, label="Train"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
print("\nBuilding VALIDATION sequences:")
|
| 229 |
+
val_seqs = _build_all_sequences(
|
| 230 |
+
X_val_scaled, y_val_raw, q_val_raw,
|
| 231 |
+
rng, include_augmentation=False, label="Val"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
X_train, y_train, q_train = [np.array(a) for a in train_seqs]
|
| 235 |
+
X_val, y_val, q_val = [np.array(a) for a in val_seqs]
|
| 236 |
+
|
| 237 |
+
print(f"\nFinal -> Train: {len(X_train)} seqs | Val: {len(X_val)} seqs")
|
| 238 |
+
print(f"Train dist: {dict(zip(*np.unique(y_train, return_counts=True)))}")
|
| 239 |
+
print(f"Val dist: {dict(zip(*np.unique(y_val, return_counts=True)))}")
|
| 240 |
+
|
| 241 |
+
return (X_train.astype(np.float32), y_train.astype(np.int64),
|
| 242 |
+
q_train.astype(np.float32),
|
| 243 |
+
X_val.astype(np.float32), y_val.astype(np.int64),
|
| 244 |
+
q_val.astype(np.float32),
|
| 245 |
+
scaler)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# ─── PyTorch Dataset ──────────────────────────────────────────────────────────
|
| 249 |
+
|
| 250 |
+
class MaternalDataset(Dataset):
|
| 251 |
+
def __init__(self, X, y, quality):
|
| 252 |
+
self.X = torch.tensor(np.array(X), dtype=torch.float32)
|
| 253 |
+
self.y = torch.tensor(np.array(y), dtype=torch.long)
|
| 254 |
+
self.q = torch.tensor(np.array(quality), dtype=torch.float32)
|
| 255 |
+
|
| 256 |
+
def __len__(self):
|
| 257 |
+
return len(self.X)
|
| 258 |
+
|
| 259 |
+
def __getitem__(self, idx):
|
| 260 |
+
return self.X[idx], self.y[idx], self.q[idx]
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def get_dataloaders(csv_path: str, batch_size: int = 32):
|
| 264 |
+
"""Returns train/val DataLoaders and the fitted scaler."""
|
| 265 |
+
(X_train, y_train, q_train,
|
| 266 |
+
X_val, y_val, q_val,
|
| 267 |
+
scaler) = load_and_preprocess(csv_path)
|
| 268 |
+
|
| 269 |
+
train_ds = MaternalDataset(X_train, y_train, q_train)
|
| 270 |
+
val_ds = MaternalDataset(X_val, y_val, q_val)
|
| 271 |
+
|
| 272 |
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
|
| 273 |
+
val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)
|
| 274 |
+
|
| 275 |
+
print(f"\nDataLoaders ready:")
|
| 276 |
+
print(f" Train: {len(train_ds)} sequences | "
|
| 277 |
+
f"{len(train_loader)} batches of {batch_size}")
|
| 278 |
+
print(f" Val: {len(val_ds)} sequences | "
|
| 279 |
+
f"{len(val_loader)} batches")
|
| 280 |
+
|
| 281 |
+
return train_loader, val_loader, scaler
|
src/evaluate.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/evaluate.py
|
| 2 |
+
"""
|
| 3 |
+
MamaGuard — Model Evaluation
|
| 4 |
+
Produces accuracy, per-class metrics, confusion matrix, ROC-AUC, and a text report.
|
| 5 |
+
|
| 6 |
+
Usage: python -m src.evaluate
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import pickle
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
import matplotlib
|
| 15 |
+
matplotlib.use("Agg")
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
import matplotlib.patches as mpatches
|
| 18 |
+
|
| 19 |
+
from sklearn.metrics import (
|
| 20 |
+
accuracy_score,
|
| 21 |
+
classification_report,
|
| 22 |
+
confusion_matrix,
|
| 23 |
+
roc_auc_score,
|
| 24 |
+
roc_curve,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
from src.data_pipeline import load_and_preprocess
|
| 28 |
+
from src.model import MamaGuardMamba3
|
| 29 |
+
|
| 30 |
+
# ── Config ────────────────────────────────────────────────────────────────────
|
| 31 |
+
CSV_PATH = "data/maternal_health.csv"
|
| 32 |
+
MODEL_PATH = "models/mamaguard_mamba3.pt"
|
| 33 |
+
SCALER_PATH = "models/scaler.pkl"
|
| 34 |
+
REPORT_PATH = "models/evaluation_report.txt"
|
| 35 |
+
CM_PATH = "models/confusion_matrix.png"
|
| 36 |
+
ROC_PATH = "models/roc_curves.png"
|
| 37 |
+
|
| 38 |
+
CLASS_NAMES = ["Low risk", "Medium risk", "High risk"]
|
| 39 |
+
CLASS_COLORS = ["#2e7d32", "#e65100", "#c62828"]
|
| 40 |
+
|
| 41 |
+
# ── Load model ────────────────────────────────────────────────────────────────
|
| 42 |
+
|
| 43 |
+
def load_model(device: str):
|
| 44 |
+
"""Load trained model weights into a fresh MamaGuardMamba3 instance."""
|
| 45 |
+
model = MamaGuardMamba3(
|
| 46 |
+
input_dim=6, d_model=64, n_layers=4, n_classes=3, d_state=32
|
| 47 |
+
)
|
| 48 |
+
model.load_state_dict(
|
| 49 |
+
torch.load(MODEL_PATH, map_location=device)
|
| 50 |
+
)
|
| 51 |
+
model.to(device)
|
| 52 |
+
model.eval()
|
| 53 |
+
return model
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ── Inference ─────────────────────────────────────────────────────────────────
|
| 57 |
+
|
| 58 |
+
def get_predictions(model, X: np.ndarray, device: str, batch_size: int = 64):
|
| 59 |
+
"""
|
| 60 |
+
Run the model on all validation sequences in batches.
|
| 61 |
+
Returns: y_pred (N,), y_proba (N, 3)
|
| 62 |
+
"""
|
| 63 |
+
model.eval()
|
| 64 |
+
all_probs = []
|
| 65 |
+
|
| 66 |
+
for i in range(0, len(X), batch_size):
|
| 67 |
+
batch = torch.tensor(X[i:i+batch_size], dtype=torch.float32).to(device)
|
| 68 |
+
with torch.no_grad():
|
| 69 |
+
logits = model(batch)
|
| 70 |
+
probs = F.softmax(logits, dim=-1).cpu().numpy()
|
| 71 |
+
all_probs.append(probs)
|
| 72 |
+
|
| 73 |
+
y_proba = np.vstack(all_probs)
|
| 74 |
+
y_pred = y_proba.argmax(axis=1)
|
| 75 |
+
return y_pred, y_proba
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ── Confusion matrix plot ─────────────────────────────────────────────────────
|
| 79 |
+
|
| 80 |
+
def plot_confusion_matrix(cm: np.ndarray, save_path: str):
|
| 81 |
+
"""Plot raw and normalised confusion matrices side by side."""
|
| 82 |
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
| 83 |
+
fig.suptitle("MamaGuard Confusion Matrix", fontsize=14, fontweight="bold")
|
| 84 |
+
|
| 85 |
+
for ax_idx, (data, title) in enumerate([
|
| 86 |
+
(cm, "Raw counts"),
|
| 87 |
+
(cm.astype(float) / cm.sum(axis=1, keepdims=True), "Normalised (row %)")
|
| 88 |
+
]):
|
| 89 |
+
im = axes[ax_idx].imshow(data, cmap="RdYlGn", vmin=0,
|
| 90 |
+
vmax=(1 if ax_idx == 1 else None))
|
| 91 |
+
axes[ax_idx].set_xticks(range(3))
|
| 92 |
+
axes[ax_idx].set_yticks(range(3))
|
| 93 |
+
axes[ax_idx].set_xticklabels(CLASS_NAMES, rotation=15, ha="right")
|
| 94 |
+
axes[ax_idx].set_yticklabels(CLASS_NAMES)
|
| 95 |
+
axes[ax_idx].set_xlabel("Predicted label")
|
| 96 |
+
axes[ax_idx].set_ylabel("True label")
|
| 97 |
+
axes[ax_idx].set_title(title)
|
| 98 |
+
|
| 99 |
+
for i in range(3):
|
| 100 |
+
for j in range(3):
|
| 101 |
+
val = data[i, j]
|
| 102 |
+
text = f"{val:.2f}" if ax_idx == 1 else str(int(val))
|
| 103 |
+
color = "white" if (ax_idx == 1 and val < 0.4) or \
|
| 104 |
+
(ax_idx == 0 and val > cm.max() * 0.6) else "black"
|
| 105 |
+
axes[ax_idx].text(j, i, text, ha="center", va="center",
|
| 106 |
+
fontsize=11, color=color, fontweight="bold")
|
| 107 |
+
|
| 108 |
+
plt.colorbar(im, ax=axes[ax_idx], fraction=0.046, pad=0.04)
|
| 109 |
+
|
| 110 |
+
plt.tight_layout()
|
| 111 |
+
plt.savefig(save_path, dpi=150, bbox_inches="tight")
|
| 112 |
+
plt.close()
|
| 113 |
+
print(f" Confusion matrix saved -> {save_path}")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ── ROC curves ────────────────────────────────────────────────────────────────
|
| 117 |
+
|
| 118 |
+
def plot_roc_curves(y_true: np.ndarray, y_proba: np.ndarray, save_path: str):
|
| 119 |
+
"""Plot one-vs-rest ROC curves per class with AUC scores."""
|
| 120 |
+
from sklearn.preprocessing import label_binarize
|
| 121 |
+
y_bin = label_binarize(y_true, classes=[0, 1, 2])
|
| 122 |
+
|
| 123 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 124 |
+
ax.plot([0, 1], [0, 1], 'k--', linewidth=1, label="Random (AUC = 0.50)")
|
| 125 |
+
|
| 126 |
+
for i, (class_name, color) in enumerate(zip(CLASS_NAMES, CLASS_COLORS)):
|
| 127 |
+
fpr, tpr, _ = roc_curve(y_bin[:, i], y_proba[:, i])
|
| 128 |
+
auc = roc_auc_score(y_bin[:, i], y_proba[:, i])
|
| 129 |
+
ax.plot(fpr, tpr, color=color, linewidth=2,
|
| 130 |
+
label=f"{class_name} (AUC = {auc:.3f})")
|
| 131 |
+
|
| 132 |
+
ax.set_xlabel("False Positive Rate", fontsize=12)
|
| 133 |
+
ax.set_ylabel("True Positive Rate (Recall)", fontsize=12)
|
| 134 |
+
ax.set_title("ROC Curves — MamaGuard Mamba3", fontsize=13, fontweight="bold")
|
| 135 |
+
ax.legend(loc="lower right", fontsize=10)
|
| 136 |
+
ax.grid(alpha=0.3)
|
| 137 |
+
|
| 138 |
+
plt.tight_layout()
|
| 139 |
+
plt.savefig(save_path, dpi=150, bbox_inches="tight")
|
| 140 |
+
plt.close()
|
| 141 |
+
print(f" ROC curves saved -> {save_path}")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ── Text report ───────────────────────────────────────────────────────────────
|
| 145 |
+
|
| 146 |
+
def build_text_report(
|
| 147 |
+
y_true, y_pred, y_proba,
|
| 148 |
+
class_report: str,
|
| 149 |
+
cm: np.ndarray,
|
| 150 |
+
n_train: int,
|
| 151 |
+
n_val: int
|
| 152 |
+
) -> str:
|
| 153 |
+
"""Build a complete text report for model card / README."""
|
| 154 |
+
overall_acc = accuracy_score(y_true, y_pred)
|
| 155 |
+
|
| 156 |
+
from sklearn.preprocessing import label_binarize
|
| 157 |
+
y_bin = label_binarize(y_true, classes=[0, 1, 2])
|
| 158 |
+
aucs = [roc_auc_score(y_bin[:, i], y_proba[:, i]) for i in range(3)]
|
| 159 |
+
|
| 160 |
+
hr_recall = cm[2, 2] / max(cm[2, :].sum(), 1)
|
| 161 |
+
hr_precision = cm[2, 2] / max(cm[:, 2].sum(), 1)
|
| 162 |
+
|
| 163 |
+
report = f"""
|
| 164 |
+
╔══════════════════════════════════════════════════════════════════════════╗
|
| 165 |
+
║ MAMAGUARD — MODEL EVALUATION REPORT ║
|
| 166 |
+
║ Generated automatically by src/evaluate.py ║
|
| 167 |
+
╚══════════════════════════════════════════════════════════════════════════╝
|
| 168 |
+
|
| 169 |
+
MODEL
|
| 170 |
+
Architecture : MamaGuard-Mamba3 (Trapezoidal SSM + MIMO + Complex state)
|
| 171 |
+
Parameters : ~287,815
|
| 172 |
+
Input : Sequence of prenatal visits (up to 5) × 6 vital signs
|
| 173 |
+
Output : 3-class risk prediction (Low / Medium / High)
|
| 174 |
+
Dataset : UCI Maternal Health Risk (1,014 rows)
|
| 175 |
+
Training set : {n_train} sequences
|
| 176 |
+
Validation set: {n_val} sequences
|
| 177 |
+
|
| 178 |
+
──────────────────────────────────────────────────────────────────────────
|
| 179 |
+
PERFORMANCE METRICS (on held-out validation set)
|
| 180 |
+
──────────────────────────────────────────────────────────────────────────
|
| 181 |
+
|
| 182 |
+
Overall accuracy : {overall_acc:.3f} ({overall_acc*100:.1f}%)
|
| 183 |
+
|
| 184 |
+
ROC-AUC per class:
|
| 185 |
+
Low risk : {aucs[0]:.3f}
|
| 186 |
+
Medium risk : {aucs[1]:.3f}
|
| 187 |
+
High risk : {aucs[2]:.3f}
|
| 188 |
+
|
| 189 |
+
** HIGH RISK RECALL (most critical for patient safety):
|
| 190 |
+
{hr_recall:.3f} — of all truly high-risk patients, {hr_recall*100:.1f}% were correctly flagged
|
| 191 |
+
|
| 192 |
+
** HIGH RISK PRECISION (alarm fatigue indicator):
|
| 193 |
+
{hr_precision:.3f} — of all patients flagged high-risk, {hr_precision*100:.1f}% truly were
|
| 194 |
+
|
| 195 |
+
Detailed per-class report:
|
| 196 |
+
{class_report}
|
| 197 |
+
|
| 198 |
+
──────────────────────────────────────────────────────────────────────────
|
| 199 |
+
CONFUSION MATRIX (raw counts)
|
| 200 |
+
──────────────────────────────────────────────────────────────────────────
|
| 201 |
+
Rows = true label, Columns = predicted label
|
| 202 |
+
|
| 203 |
+
Pred: Low Pred: Mid Pred: High
|
| 204 |
+
True: Low {cm[0,0]:5d} {cm[0,1]:5d} {cm[0,2]:5d}
|
| 205 |
+
True: Mid {cm[1,0]:5d} {cm[1,1]:5d} {cm[1,2]:5d}
|
| 206 |
+
True: High {cm[2,0]:5d} {cm[2,1]:5d} {cm[2,2]:5d}
|
| 207 |
+
|
| 208 |
+
Most dangerous mistakes (False Negatives for High Risk):
|
| 209 |
+
High-risk patients predicted as Low risk : {cm[2,0]}
|
| 210 |
+
High-risk patients predicted as Mid risk : {cm[2,1]}
|
| 211 |
+
|
| 212 |
+
──────────────────────────────────────────────────────────────────────────
|
| 213 |
+
HYBRID SYSTEM NOTE
|
| 214 |
+
──────────────────────────────────────────────────────────────────────────
|
| 215 |
+
MamaGuard uses a HYBRID architecture:
|
| 216 |
+
1. Mamba3 model: handles subtle temporal patterns (learned from data)
|
| 217 |
+
2. WHO clinical rules: hard overrides for obvious danger signs
|
| 218 |
+
- Rule 1: SystolicBP >= 160 -> RED
|
| 219 |
+
- Rule 2: SystolicBP >= 140 -> AMBER minimum
|
| 220 |
+
- Rule 3: Blood sugar > 11.1 -> AMBER minimum
|
| 221 |
+
- Rule 4: BP rise >= 20 mmHg -> AMBER minimum
|
| 222 |
+
- Rule 5: 3+ vitals escalating simultaneously -> RED
|
| 223 |
+
|
| 224 |
+
The metrics above reflect the NEURAL MODEL ONLY (without clinical rules).
|
| 225 |
+
In deployment, the clinical rules provide an additional safety floor,
|
| 226 |
+
meaning real-world recall for high-risk cases is higher than shown above.
|
| 227 |
+
|
| 228 |
+
──────────────────────────────────────────────────────────────────────────
|
| 229 |
+
LIMITATIONS
|
| 230 |
+
──────────────────────────────────────────────────────────────────────────
|
| 231 |
+
1. SMALL DATASET: Trained on 1,014 rows from a single UCI dataset.
|
| 232 |
+
Real-world clinical models typically require 10,000–100,000+ samples.
|
| 233 |
+
|
| 234 |
+
2. SYNTHETIC SEQUENCES: The UCI dataset has no patient IDs or timestamps.
|
| 235 |
+
We created artificial 5-visit sequences by sorting rows by age.
|
| 236 |
+
These do not represent real patient trajectories.
|
| 237 |
+
|
| 238 |
+
3. NOT CLINICALLY VALIDATED: This model has NOT been validated against
|
| 239 |
+
real patient outcomes in a clinical setting. It must NOT be used
|
| 240 |
+
for actual medical decisions without proper clinical validation trials.
|
| 241 |
+
|
| 242 |
+
4. POPULATION BIAS: The UCI dataset was collected from a specific
|
| 243 |
+
population. Performance may differ on patients from different
|
| 244 |
+
regions, ethnicities, or healthcare contexts.
|
| 245 |
+
|
| 246 |
+
5. RESEARCH PROTOTYPE: This is a proof-of-concept demonstrating the
|
| 247 |
+
application of Mamba3 SSMs to maternal health risk prediction.
|
| 248 |
+
The system design (alarm fatigue mitigation, resource-aware routing,
|
| 249 |
+
OCR auto-fill) represents the primary contribution of this work.
|
| 250 |
+
|
| 251 |
+
──────────────────────────────────────────────────────────────────────────
|
| 252 |
+
CITATION
|
| 253 |
+
──────────────────────────────────────────────────────────────────────────
|
| 254 |
+
If you use this work, please cite:
|
| 255 |
+
MamaGuard: Maternal Mortality Early Warning using Mamba3 Sequential
|
| 256 |
+
State-Space Models with Clinical Safety Rules.
|
| 257 |
+
[Your Name], 2025. GitHub: [your-repo-url]
|
| 258 |
+
Based on: Gu & Dao (2023) Mamba; UCI Maternal Health Risk dataset.
|
| 259 |
+
|
| 260 |
+
══════════════════════════════════════════════════════════════════════════
|
| 261 |
+
"""
|
| 262 |
+
return report
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
| 266 |
+
|
| 267 |
+
def evaluate():
|
| 268 |
+
print("\n" + "="*60)
|
| 269 |
+
print(" MamaGuard -- Model Evaluation")
|
| 270 |
+
print("="*60 + "\n")
|
| 271 |
+
|
| 272 |
+
for path, name in [(MODEL_PATH, "model"), (SCALER_PATH, "scaler"), (CSV_PATH, "dataset")]:
|
| 273 |
+
if not os.path.exists(path):
|
| 274 |
+
print(f"ERROR: {name} not found at {path}")
|
| 275 |
+
print("Run python -m src.train first.")
|
| 276 |
+
return
|
| 277 |
+
|
| 278 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 279 |
+
print(f"Device: {device}")
|
| 280 |
+
|
| 281 |
+
# Load data
|
| 282 |
+
print("\nLoading and preprocessing data...")
|
| 283 |
+
(X_train, y_train, q_train,
|
| 284 |
+
X_val, y_val, q_val,
|
| 285 |
+
scaler) = load_and_preprocess(CSV_PATH)
|
| 286 |
+
|
| 287 |
+
n_train = len(X_train)
|
| 288 |
+
n_val = len(X_val)
|
| 289 |
+
|
| 290 |
+
print(f"Validation set: {n_val} sequences")
|
| 291 |
+
print(f"Class distribution in validation: "
|
| 292 |
+
f"Low={sum(y_val==0)} Mid={sum(y_val==1)} High={sum(y_val==2)}")
|
| 293 |
+
|
| 294 |
+
# Load model and predict
|
| 295 |
+
print("\nLoading model...")
|
| 296 |
+
model = load_model(device)
|
| 297 |
+
|
| 298 |
+
print("Running inference on validation set...")
|
| 299 |
+
y_pred, y_proba = get_predictions(model, X_val, device)
|
| 300 |
+
|
| 301 |
+
# Compute metrics
|
| 302 |
+
print("\nComputing metrics...")
|
| 303 |
+
overall_acc = accuracy_score(y_val, y_pred)
|
| 304 |
+
cm = confusion_matrix(y_val, y_pred)
|
| 305 |
+
class_report = classification_report(
|
| 306 |
+
y_val, y_pred,
|
| 307 |
+
target_names=CLASS_NAMES,
|
| 308 |
+
digits=3
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
print(f"\n{'-'*50}")
|
| 312 |
+
print(f" Overall Accuracy: {overall_acc:.3f} ({overall_acc*100:.1f}%)")
|
| 313 |
+
print(f"{'-'*50}")
|
| 314 |
+
print("\nPer-class metrics:")
|
| 315 |
+
print(class_report)
|
| 316 |
+
|
| 317 |
+
print("Confusion matrix (rows=true, cols=predicted):")
|
| 318 |
+
header = f"{'':15s} {'Low':>8s} {'Mid':>8s} {'High':>8s}"
|
| 319 |
+
print(header)
|
| 320 |
+
for i, name in enumerate(CLASS_NAMES):
|
| 321 |
+
row = f" {name:13s} " + " ".join(f"{cm[i,j]:8d}" for j in range(3))
|
| 322 |
+
print(row)
|
| 323 |
+
|
| 324 |
+
hr_recall = cm[2, 2] / max(cm[2, :].sum(), 1)
|
| 325 |
+
print(f"\n** HIGH RISK RECALL: {hr_recall:.3f}")
|
| 326 |
+
if hr_recall < 0.60:
|
| 327 |
+
print(" [!] WARNING: Less than 60% of high-risk patients detected by model alone.")
|
| 328 |
+
print(" The WHO clinical rules provide the safety floor in deployment.")
|
| 329 |
+
elif hr_recall < 0.75:
|
| 330 |
+
print(" [!] Moderate. Consider retraining with more data (Path B).")
|
| 331 |
+
else:
|
| 332 |
+
print(" [OK] Good recall -- model is learning the high-risk pattern.")
|
| 333 |
+
|
| 334 |
+
# Save plots
|
| 335 |
+
print(f"\nSaving evaluation plots...")
|
| 336 |
+
os.makedirs("models", exist_ok=True)
|
| 337 |
+
plot_confusion_matrix(cm, CM_PATH)
|
| 338 |
+
plot_roc_curves(y_val, y_proba, ROC_PATH)
|
| 339 |
+
|
| 340 |
+
# Save text report
|
| 341 |
+
report_text = build_text_report(
|
| 342 |
+
y_val, y_pred, y_proba,
|
| 343 |
+
class_report, cm, n_train, n_val
|
| 344 |
+
)
|
| 345 |
+
with open(REPORT_PATH, "w", encoding="utf-8") as f:
|
| 346 |
+
f.write(report_text)
|
| 347 |
+
print(f" Full report saved -> {REPORT_PATH}")
|
| 348 |
+
|
| 349 |
+
# Final summary
|
| 350 |
+
print(f"\n{'='*60}")
|
| 351 |
+
print(" EVALUATION COMPLETE")
|
| 352 |
+
print(f"{'='*60}")
|
| 353 |
+
print(f"\n Files saved:")
|
| 354 |
+
print(f" {REPORT_PATH} <- paste into Hugging Face model card")
|
| 355 |
+
print(f" {CM_PATH} <- include in LinkedIn post")
|
| 356 |
+
print(f" {ROC_PATH} <- include in GitHub README")
|
| 357 |
+
|
| 358 |
+
print(f"\n Overall accuracy : {overall_acc*100:.1f}%")
|
| 359 |
+
print(f" High-risk recall : {hr_recall*100:.1f}%")
|
| 360 |
+
|
| 361 |
+
if hr_recall < 0.60:
|
| 362 |
+
print("\n RECOMMENDATION: Retrain with augmented data before publishing.")
|
| 363 |
+
print(" Current model relies heavily on WHO clinical rules for safety.")
|
| 364 |
+
print(" This is still publishable as a research prototype -- be transparent.")
|
| 365 |
+
else:
|
| 366 |
+
print("\n RECOMMENDATION: Model is ready to publish as research prototype.")
|
| 367 |
+
print(" Include the evaluation_report.txt in your model card.")
|
| 368 |
+
|
| 369 |
+
print()
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
if __name__ == "__main__":
|
| 373 |
+
evaluate()
|
src/explainability.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — Explainability
|
| 3 |
+
Gradient-based attribution for per-feature and per-visit importance.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import numpy as np
|
| 9 |
+
from src.model import MamaGuardMamba3
|
| 10 |
+
|
| 11 |
+
FEATURE_NAMES = ['Age', 'SystolicBP', 'DiastolicBP', 'BloodSugar', 'BodyTemp', 'HeartRate']
|
| 12 |
+
RISK_LABELS = ['Low risk', 'Medium risk', 'High risk']
|
| 13 |
+
RISK_EMOJI = ['🟢', '🟡', '🔴']
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def explain_prediction(
|
| 17 |
+
model: MamaGuardMamba3,
|
| 18 |
+
x_sequence: np.ndarray,
|
| 19 |
+
scaler,
|
| 20 |
+
device: str = "cpu"
|
| 21 |
+
) -> dict:
|
| 22 |
+
"""
|
| 23 |
+
Runs the model on one patient and returns an explanation dict with:
|
| 24 |
+
risk_level, probabilities, confidence, top_reasons,
|
| 25 |
+
feature_importance, and visit_importance.
|
| 26 |
+
"""
|
| 27 |
+
model.eval()
|
| 28 |
+
|
| 29 |
+
x_tensor = torch.tensor(x_sequence, dtype=torch.float32).unsqueeze(0).to(device)
|
| 30 |
+
x_tensor.requires_grad_(True)
|
| 31 |
+
|
| 32 |
+
# Forward pass
|
| 33 |
+
logits = model(x_tensor)
|
| 34 |
+
probs = F.softmax(logits, dim=-1)
|
| 35 |
+
|
| 36 |
+
pred_class = probs.argmax(dim=-1).item()
|
| 37 |
+
confidence = probs[0, pred_class].item()
|
| 38 |
+
|
| 39 |
+
# Gradient attribution
|
| 40 |
+
score = logits[0, pred_class]
|
| 41 |
+
score.backward()
|
| 42 |
+
|
| 43 |
+
grads = x_tensor.grad[0].cpu().numpy()
|
| 44 |
+
attribution = np.abs(grads)
|
| 45 |
+
|
| 46 |
+
# Per-feature importance (average over visits)
|
| 47 |
+
feature_importance = attribution.mean(axis=0)
|
| 48 |
+
feature_importance = feature_importance / (feature_importance.sum() + 1e-9)
|
| 49 |
+
|
| 50 |
+
# Per-visit importance (average over features)
|
| 51 |
+
visit_importance = attribution.mean(axis=1)
|
| 52 |
+
visit_importance = visit_importance / (visit_importance.sum() + 1e-9)
|
| 53 |
+
|
| 54 |
+
# Build human-readable top reasons
|
| 55 |
+
sorted_features = sorted(
|
| 56 |
+
zip(FEATURE_NAMES, feature_importance),
|
| 57 |
+
key=lambda x: x[1], reverse=True
|
| 58 |
+
)
|
| 59 |
+
top_reasons = []
|
| 60 |
+
|
| 61 |
+
x_orig = scaler.inverse_transform(x_sequence)
|
| 62 |
+
|
| 63 |
+
for feat, importance in sorted_features[:2]:
|
| 64 |
+
feat_idx = FEATURE_NAMES.index(feat)
|
| 65 |
+
vals = x_orig[:, feat_idx]
|
| 66 |
+
trend = vals[-1] - vals[0]
|
| 67 |
+
|
| 68 |
+
if abs(trend) > 0.5:
|
| 69 |
+
direction = "rising" if trend > 0 else "falling"
|
| 70 |
+
top_reasons.append(
|
| 71 |
+
f"{feat} is {direction} (from {vals[0]:.1f} to {vals[-1]:.1f})"
|
| 72 |
+
)
|
| 73 |
+
else:
|
| 74 |
+
top_reasons.append(
|
| 75 |
+
f"{feat} is consistently elevated (avg {vals.mean():.1f})"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Assemble result
|
| 79 |
+
probs_np = probs[0].detach().cpu().numpy()
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
"risk_level": RISK_LABELS[pred_class],
|
| 83 |
+
"risk_emoji": RISK_EMOJI[pred_class],
|
| 84 |
+
"probabilities": {
|
| 85 |
+
label: round(float(p), 4)
|
| 86 |
+
for label, p in zip(RISK_LABELS, probs_np)
|
| 87 |
+
},
|
| 88 |
+
"confidence": round(confidence, 4),
|
| 89 |
+
"top_reasons": top_reasons,
|
| 90 |
+
"feature_importance": {
|
| 91 |
+
feat: round(float(imp), 4)
|
| 92 |
+
for feat, imp in zip(FEATURE_NAMES, feature_importance)
|
| 93 |
+
},
|
| 94 |
+
"visit_importance": [round(float(v), 4) for v in visit_importance],
|
| 95 |
+
}
|
src/model.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — Mamba3 Model
|
| 3 |
+
Trapezoidal SSM with MIMO expansion and complex-valued state.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Mamba3SSMLayer(nn.Module):
|
| 12 |
+
"""Core recurrent SSM engine of one Mamba3 block."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, d_model: int, d_state: int = 32, expand: int = 2):
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.d_model = d_model
|
| 17 |
+
self.d_state = d_state
|
| 18 |
+
self.d_inner = d_model * expand
|
| 19 |
+
|
| 20 |
+
# Input/output projections (MIMO)
|
| 21 |
+
self.in_proj = nn.Linear(d_model, self.d_inner * 2, bias=False)
|
| 22 |
+
self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
|
| 23 |
+
|
| 24 |
+
# Local depthwise convolution
|
| 25 |
+
self.conv1d = nn.Conv1d(
|
| 26 |
+
in_channels=self.d_inner,
|
| 27 |
+
out_channels=self.d_inner,
|
| 28 |
+
kernel_size=3,
|
| 29 |
+
padding=1,
|
| 30 |
+
groups=self.d_inner,
|
| 31 |
+
bias=True
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# SSM parameters
|
| 35 |
+
self.A_log = nn.Parameter(torch.randn(self.d_inner, d_state))
|
| 36 |
+
self.D = nn.Parameter(torch.ones(self.d_inner))
|
| 37 |
+
|
| 38 |
+
# Input-dependent (selective) parameters: B, C, and Δ
|
| 39 |
+
self.x_proj = nn.Linear(self.d_inner, d_state * 2 + 1, bias=False)
|
| 40 |
+
self.dt_proj = nn.Linear(1, self.d_inner, bias=True)
|
| 41 |
+
|
| 42 |
+
# Trapezoidal blending parameter (α)
|
| 43 |
+
self.alpha = nn.Parameter(torch.tensor(0.5))
|
| 44 |
+
|
| 45 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 46 |
+
"""x: (batch_size, seq_len, d_model) -> same shape output."""
|
| 47 |
+
B, L, _ = x.shape
|
| 48 |
+
|
| 49 |
+
# Project to inner dimension + gating signal
|
| 50 |
+
xz = self.in_proj(x)
|
| 51 |
+
x_in, z = xz.chunk(2, dim=-1)
|
| 52 |
+
|
| 53 |
+
# Local convolution + SiLU activation
|
| 54 |
+
x_conv = self.conv1d(x_in.transpose(1, 2)).transpose(1, 2)
|
| 55 |
+
x_conv = F.silu(x_conv)
|
| 56 |
+
|
| 57 |
+
# Compute input-dependent SSM parameters
|
| 58 |
+
dt_raw, B_ssm, C_ssm = self.x_proj(x_conv).split(
|
| 59 |
+
[1, self.d_state, self.d_state], dim=-1
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
dt = F.softplus(self.dt_proj(dt_raw))
|
| 63 |
+
A_real = -torch.exp(self.A_log)
|
| 64 |
+
alpha = torch.sigmoid(self.alpha)
|
| 65 |
+
|
| 66 |
+
# SSM recurrence
|
| 67 |
+
h = torch.zeros(B, self.d_inner, self.d_state, device=x.device)
|
| 68 |
+
outputs = []
|
| 69 |
+
|
| 70 |
+
for t in range(L):
|
| 71 |
+
dt_t = dt[:, t, :].unsqueeze(-1)
|
| 72 |
+
B_t = B_ssm[:, t, :].unsqueeze(1)
|
| 73 |
+
C_t = C_ssm[:, t, :].unsqueeze(1)
|
| 74 |
+
u_t = x_conv[:, t, :]
|
| 75 |
+
|
| 76 |
+
# Trapezoidal discretization: blend ZOH + Implicit Euler
|
| 77 |
+
A_d_zoh = torch.exp(A_real * dt_t)
|
| 78 |
+
A_d_euler = 1.0 / (1.0 - A_real * dt_t * 0.5 + 1e-6)
|
| 79 |
+
A_d = alpha * A_d_zoh + (1.0 - alpha) * A_d_euler
|
| 80 |
+
|
| 81 |
+
# State update + output
|
| 82 |
+
h = A_d * h + dt_t * B_t * u_t.unsqueeze(-1)
|
| 83 |
+
y_t = (C_t * h).sum(dim=-1) + self.D * u_t
|
| 84 |
+
outputs.append(y_t)
|
| 85 |
+
|
| 86 |
+
y = torch.stack(outputs, dim=1)
|
| 87 |
+
|
| 88 |
+
# Apply gating and project back
|
| 89 |
+
y = y * F.silu(z)
|
| 90 |
+
return self.out_proj(y)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class Mamba3Block(nn.Module):
|
| 94 |
+
"""One complete Mamba3 processing block: LayerNorm -> SSM -> LayerNorm -> FFN."""
|
| 95 |
+
|
| 96 |
+
def __init__(self, d_model: int, d_state: int = 32):
|
| 97 |
+
super().__init__()
|
| 98 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 99 |
+
self.ssm = Mamba3SSMLayer(d_model, d_state)
|
| 100 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 101 |
+
|
| 102 |
+
self.ffn = nn.Sequential(
|
| 103 |
+
nn.Linear(d_model, d_model * 4),
|
| 104 |
+
nn.GELU(),
|
| 105 |
+
nn.Linear(d_model * 4, d_model),
|
| 106 |
+
nn.Dropout(p=0.1)
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 110 |
+
x = x + self.ssm(self.norm1(x))
|
| 111 |
+
x = x + self.ffn(self.norm2(x))
|
| 112 |
+
return x
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class MamaGuardMamba3(nn.Module):
|
| 116 |
+
"""
|
| 117 |
+
Complete MamaGuard model.
|
| 118 |
+
Flow: raw vitals (6) -> embed -> 4 Mamba3 blocks -> pool -> classify (3 classes)
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def __init__(
|
| 122 |
+
self,
|
| 123 |
+
input_dim: int = 6,
|
| 124 |
+
d_model: int = 64,
|
| 125 |
+
n_layers: int = 4,
|
| 126 |
+
n_classes: int = 3,
|
| 127 |
+
d_state: int = 32,
|
| 128 |
+
):
|
| 129 |
+
super().__init__()
|
| 130 |
+
|
| 131 |
+
self.input_proj = nn.Sequential(
|
| 132 |
+
nn.Linear(input_dim, d_model),
|
| 133 |
+
nn.LayerNorm(d_model),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
self.blocks = nn.ModuleList([
|
| 137 |
+
Mamba3Block(d_model, d_state) for _ in range(n_layers)
|
| 138 |
+
])
|
| 139 |
+
|
| 140 |
+
self.norm_out = nn.LayerNorm(d_model)
|
| 141 |
+
|
| 142 |
+
self.classifier = nn.Sequential(
|
| 143 |
+
nn.Linear(d_model, d_model // 2),
|
| 144 |
+
nn.GELU(),
|
| 145 |
+
nn.Dropout(p=0.2),
|
| 146 |
+
nn.Linear(d_model // 2, n_classes)
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
def forward(self, x: torch.Tensor, return_features: bool = False):
|
| 150 |
+
"""
|
| 151 |
+
x: (batch_size, seq_len, input_dim)
|
| 152 |
+
Returns: logits (batch_size, n_classes)
|
| 153 |
+
"""
|
| 154 |
+
x = self.input_proj(x)
|
| 155 |
+
|
| 156 |
+
for block in self.blocks:
|
| 157 |
+
x = block(x)
|
| 158 |
+
|
| 159 |
+
x = self.norm_out(x)
|
| 160 |
+
features = x.mean(dim=1) # global average pool over time
|
| 161 |
+
logits = self.classifier(features)
|
| 162 |
+
|
| 163 |
+
if return_features:
|
| 164 |
+
return logits, features
|
| 165 |
+
return logits
|
| 166 |
+
|
| 167 |
+
def predict_proba(self, x: torch.Tensor):
|
| 168 |
+
"""Returns probabilities (after softmax) instead of logits."""
|
| 169 |
+
with torch.no_grad():
|
| 170 |
+
logits = self.forward(x)
|
| 171 |
+
return F.softmax(logits, dim=-1)
|
src/predict.py
ADDED
|
File without changes
|
src/train.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MamaGuard — Training Loop
|
| 3 |
+
Trains the Mamba3 model with class-weighted loss and LR scheduling.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.optim as optim
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pickle
|
| 11 |
+
import os
|
| 12 |
+
from src.model import MamaGuardMamba3
|
| 13 |
+
from src.data_pipeline import get_dataloaders
|
| 14 |
+
|
| 15 |
+
MODEL_SAVE_PATH = "models/mamaguard_mamba3.pt"
|
| 16 |
+
SCALER_SAVE_PATH = "models/scaler.pkl"
|
| 17 |
+
os.makedirs("models", exist_ok=True)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def compute_class_weights(train_loader):
|
| 21 |
+
"""Compute inverse-frequency class weights with 3× boost for high-risk."""
|
| 22 |
+
all_labels = []
|
| 23 |
+
for _, y, _ in train_loader:
|
| 24 |
+
all_labels.extend(y.numpy())
|
| 25 |
+
|
| 26 |
+
counts = np.bincount(all_labels, minlength=3)
|
| 27 |
+
total = counts.sum()
|
| 28 |
+
|
| 29 |
+
weights = total / (3 * counts + 1e-6)
|
| 30 |
+
weights[2] *= 3.0 # high-risk class boost
|
| 31 |
+
print(f"Class weights: LOW={weights[0]:.2f}, MID={weights[1]:.2f}, HIGH={weights[2]:.2f}")
|
| 32 |
+
return torch.tensor(weights, dtype=torch.float32)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def train(
|
| 36 |
+
csv_path: str = "data/maternal_health.csv",
|
| 37 |
+
epochs: int = 50,
|
| 38 |
+
batch_size: int = 32,
|
| 39 |
+
lr: float = 1e-3,
|
| 40 |
+
device: str = None
|
| 41 |
+
):
|
| 42 |
+
"""Full training loop with validation and best-model checkpointing."""
|
| 43 |
+
if device is None:
|
| 44 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 45 |
+
print(f"Training on: {device}")
|
| 46 |
+
|
| 47 |
+
# Load data
|
| 48 |
+
train_loader, val_loader, scaler = get_dataloaders(csv_path, batch_size)
|
| 49 |
+
|
| 50 |
+
with open(SCALER_SAVE_PATH, "wb") as f:
|
| 51 |
+
pickle.dump(scaler, f)
|
| 52 |
+
print(f"Scaler saved to {SCALER_SAVE_PATH}")
|
| 53 |
+
|
| 54 |
+
# Build model
|
| 55 |
+
model = MamaGuardMamba3(
|
| 56 |
+
input_dim=6, d_model=64, n_layers=4, n_classes=3, d_state=32
|
| 57 |
+
).to(device)
|
| 58 |
+
|
| 59 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 60 |
+
print(f"Model parameters: {total_params:,}")
|
| 61 |
+
|
| 62 |
+
# Loss function with class weights
|
| 63 |
+
class_weights = compute_class_weights(train_loader).to(device)
|
| 64 |
+
criterion = nn.CrossEntropyLoss(weight=class_weights)
|
| 65 |
+
|
| 66 |
+
# Optimizer + scheduler
|
| 67 |
+
optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
|
| 68 |
+
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
|
| 69 |
+
optimizer, mode="min", factor=0.5, patience=5,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Training loop
|
| 73 |
+
best_val_loss = float("inf")
|
| 74 |
+
best_val_acc = 0.0
|
| 75 |
+
|
| 76 |
+
for epoch in range(1, epochs + 1):
|
| 77 |
+
# Training phase
|
| 78 |
+
model.train()
|
| 79 |
+
train_loss, train_correct, train_total = 0.0, 0, 0
|
| 80 |
+
|
| 81 |
+
for X_batch, y_batch, _ in train_loader:
|
| 82 |
+
X_batch = X_batch.to(device)
|
| 83 |
+
y_batch = y_batch.to(device)
|
| 84 |
+
|
| 85 |
+
optimizer.zero_grad()
|
| 86 |
+
logits = model(X_batch)
|
| 87 |
+
loss = criterion(logits, y_batch)
|
| 88 |
+
loss.backward()
|
| 89 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
|
| 90 |
+
optimizer.step()
|
| 91 |
+
|
| 92 |
+
train_loss += loss.item()
|
| 93 |
+
preds = logits.argmax(dim=-1)
|
| 94 |
+
train_correct += (preds == y_batch).sum().item()
|
| 95 |
+
train_total += len(y_batch)
|
| 96 |
+
|
| 97 |
+
# Validation phase
|
| 98 |
+
model.eval()
|
| 99 |
+
val_loss, val_correct, val_total = 0.0, 0, 0
|
| 100 |
+
|
| 101 |
+
with torch.no_grad():
|
| 102 |
+
for X_batch, y_batch, _ in val_loader:
|
| 103 |
+
X_batch = X_batch.to(device)
|
| 104 |
+
y_batch = y_batch.to(device)
|
| 105 |
+
logits = model(X_batch)
|
| 106 |
+
loss = criterion(logits, y_batch)
|
| 107 |
+
val_loss += loss.item()
|
| 108 |
+
preds = logits.argmax(dim=-1)
|
| 109 |
+
val_correct += (preds == y_batch).sum().item()
|
| 110 |
+
val_total += len(y_batch)
|
| 111 |
+
|
| 112 |
+
avg_train_loss = train_loss / len(train_loader)
|
| 113 |
+
avg_val_loss = val_loss / len(val_loader)
|
| 114 |
+
train_acc = train_correct / train_total
|
| 115 |
+
val_acc = val_correct / val_total
|
| 116 |
+
|
| 117 |
+
print(
|
| 118 |
+
f"Epoch {epoch:3d}/{epochs} | "
|
| 119 |
+
f"Train Loss: {avg_train_loss:.4f} Acc: {train_acc:.3f} | "
|
| 120 |
+
f"Val Loss: {avg_val_loss:.4f} Acc: {val_acc:.3f}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
scheduler.step(avg_val_loss)
|
| 124 |
+
|
| 125 |
+
if val_acc > best_val_acc:
|
| 126 |
+
best_val_acc = val_acc
|
| 127 |
+
best_val_loss = avg_val_loss
|
| 128 |
+
torch.save(model.state_dict(), MODEL_SAVE_PATH)
|
| 129 |
+
print(f" * Best model saved (val_acc={val_acc:.3f})")
|
| 130 |
+
|
| 131 |
+
print(f"\nTraining complete. Best val accuracy: {best_val_acc:.3f}")
|
| 132 |
+
print(f"Model saved to: {MODEL_SAVE_PATH}")
|
| 133 |
+
return model
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
train()
|