Skip to content

Serving

The main and baseline models are exposed behind a single FastAPI app (cardio_risk_rf.serving.main:app). Both are loaded lazily from artifacts/{main,baseline}/*.joblib and selected via a query parameter.

Run

# local dev
uv run uvicorn cardio_risk_rf.serving.main:app --host 0.0.0.0 --port 8000

# docker
docker run --rm -p 8000:8000 ghcr.io/kiselyovd/cardio-risk-rf:v0.1.0

Endpoints

Method Path Purpose
GET /health Liveness probe — returns {"status": "ok", "version": <model_version>}.
POST /predict Score a single patient. Query param model selects main (default) or baseline.

/predict returns HTTP 422 if every feature is null, and HTTP 503 if the joblib checkpoint is missing from disk.

Request — PatientFeatures

Schema source: cardio_risk_rf.serving.schemas.PatientFeatures. All fields are optional — missing values are forwarded as NaN into the pipeline (LightGBM main handles this natively; the baseline imputes with the train-set median).

class PatientFeatures(BaseModel):
    male: int | None              # 0 or 1
    age: int | None               # years, 0–120
    education: float | None       # 1–4 (Framingham encoding)
    currentSmoker: int | None     # 0 or 1
    cigsPerDay: float | None      # cigarettes per day
    BPMeds: float | None          # 0 or 1 — on BP medication
    prevalentStroke: int | None   # 0 or 1
    prevalentHyp: int | None      # 0 or 1 — prevalent hypertension
    diabetes: int | None          # 0 or 1
    totChol: float | None         # total cholesterol (mg/dL)
    sysBP: float | None           # systolic BP (mmHg)
    diaBP: float | None           # diastolic BP (mmHg)
    BMI: float | None             # kg/m²
    heartRate: float | None       # bpm
    glucose: float | None         # mg/dL

Response — PredictionResponse

class ShapEntry(BaseModel):
    feature: str
    value: float | int | None
    shap: float


class PredictionResponse(BaseModel):
    probability: float            # P(TenYearCHD=1)
    cls: int                      # 0 or 1, serialised as "class"
    threshold: float              # decision threshold used (default 0.5)
    shap_top5: list[ShapEntry]    # 5 features with largest |SHAP| on this request
    model_version: str            # e.g. "v0.1.0"
    model_name: str               # "cardio_risk_lgbm" or "cardio_risk_rf"
    request_id: str               # 12-char UUID prefix for tracing

curl example

curl -X POST 'localhost:8000/predict?model=main' \
  -H 'content-type: application/json' \
  -d @data/widget/sample_patient.json

Using the main model on the bundled sample patient, a typical response looks like:

{
  "probability": 0.18,
  "class": 0,
  "threshold": 0.5,
  "shap_top5": [
    {"feature": "age", "value": 52, "shap": 0.42},
    {"feature": "sysBP", "value": 138, "shap": 0.21},
    {"feature": "cigsPerDay", "value": 10, "shap": 0.14},
    {"feature": "totChol", "value": 220, "shap": -0.08},
    {"feature": "BMI", "value": 24.1, "shap": -0.05}
  ],
  "model_version": "v0.1.0",
  "model_name": "cardio_risk_lgbm",
  "request_id": "a1b2c3d4e5f6"
}

To score the same patient with the baseline RandomForest, pass ?model=baseline.

Configuration

Environment variables read by cardio_risk_rf.serving.routes:

Variable Default Purpose
CARDIO_MAIN_CKPT artifacts/main/cardio_risk_lgbm.joblib Main model path.
CARDIO_BASELINE_CKPT artifacts/baseline/cardio_risk_rf.joblib Baseline model path.
CARDIO_MODEL_VERSION v0.1.0 Reported in /health and response body.
CARDIO_THRESHOLD 0.5 Decision threshold applied to probability.