readmitiq-api / src /api /routers /predict.py
DrKryptoMed's picture
feat: deploy ReadmitIQ API to HuggingFace Spaces
742cd9a
Raw
History Blame Contribute Delete
4.6 kB
"""
Prediction endpoints.
Thin router layer — HTTP handling only.
All business logic lives in predictor.py.
Endpoints:
POST /predict/features — predict from engineered features
GET /predict/example — return example high-risk patient
"""
import logging
from fastapi import APIRouter, HTTPException, status
from src.api.models import PatientFeatures, PredictionResponse
from src.api.predictor import predictor
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/predict",
tags=["Prediction"],
)
@router.post(
"/features",
response_model=PredictionResponse,
summary="Predict 30-day readmission risk",
description="""
Accepts 31 engineered clinical features and returns:
- Readmission probability (0.0–1.0)
- Risk category (LOW / MEDIUM / HIGH)
- Top 5 SHAP-based risk factors with clinical descriptions
- Clinical action recommendation
All features must be computed at the point of patient discharge.
Features requiring post-discharge knowledge constitute target leakage
and must not be included.
""",
status_code=status.HTTP_200_OK,
)
async def predict_from_features(
patient: PatientFeatures,
) -> PredictionResponse:
"""
Run readmission risk prediction for a single patient.
The request body should contain all 31 engineered features.
See the /predict/example endpoint for a sample high-risk patient.
"""
if not predictor.is_loaded:
logger.error("Prediction requested but model not loaded")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail={
"error": "Model not available",
"message": "The prediction model is not loaded. "
"Please try again in a few seconds.",
}
)
try:
logger.info(
f"Prediction request received — "
f"age={patient.age_at_admission}, "
f"los={patient.los_days}, "
f"charlson={patient.charlson_score}"
)
result = predictor.predict(patient)
logger.info(
f"Prediction complete — "
f"risk={result.risk_percent}%, "
f"category={result.risk_category}"
)
return result
except Exception as e:
logger.error(f"Prediction failed: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"error": "Prediction failed",
"message": "An error occurred during prediction. "
"Please check your input and try again.",
}
)
@router.get(
"/example",
response_model=dict,
summary="Get example patient input",
description="Returns the highest-risk patient from model evaluation "
"as an example API input. Useful for testing.",
)
async def get_example_patient() -> dict:
"""
Return example high-risk patient features.
This is the patient from the SHAP waterfall plot —
predicted 99.6% readmission risk, actually readmitted.
"""
return {
"description": "Highest-risk patient from model evaluation",
"predicted_risk": "99.6%",
"actual_outcome": "Readmitted",
"clinical_profile": (
"Male, age 66.6, 3 prior admissions in 6 months, "
"47 medications, Charlson score 2, 29 active conditions"
),
"features": {
"los_days": 6.0,
"los_days_log": 1.946,
"admission_month": 2,
"admission_dow": 1,
"is_emergency": 0,
"n_admissions_prior_6m": 3,
"n_admissions_prior_12m": 3,
"n_ed_visits_prior_6m": 1,
"days_since_last_admission": 28.0,
"has_prior_admission": 1,
"charlson_score": 2,
"n_active_conditions": 29,
"has_heart_failure": 0,
"has_diabetes": 0,
"has_diabetes_complex": 0,
"has_copd": 0,
"has_ckd": 1,
"has_mi": 0,
"has_cancer": 1,
"has_dementia": 0,
"has_cerebrovascular": 0,
"has_pvd": 0,
"n_medications_capped": 47.0,
"is_high_polypharmacy": 1,
"has_insulin": 0,
"has_anticoagulant": 1,
"has_diuretic": 1,
"has_ace_inhibitor": 0,
"age_at_admission": 66.6,
"gender_male": 1,
"income": 125393.0
}
}