Assignment for the Getting Started with ML in Production workshop. Workflow: Model → FastAPI → Docker → GitHub → Render → Logging & Monitoring
A scikit-learn HistGradientBoostingRegressor trained on the California housing
dataset (20,640 census blocks, 8 features) predicts the median house value of a
block. The model is saved as model.pkl and served by FastAPI.
| Metric (held-out 20%) | Value |
|---|---|
| R² | 0.849 |
| Mean absolute error | ≈ $29,400 |
.
├── train.py # trains the model, writes model.pkl
├── model.pkl # trained artifact (committed on purpose: Render needs it)
├── main.py # FastAPI app: /, /health, /predict, /metrics, /dashboard + JSON logging
├── static/dashboard.html # browser UI: run the tests, live metrics, try a prediction
├── requirements.txt # pinned runtime deps (same sklearn version trains & serves)
├── Dockerfile # python:3.11-slim image, honours Render's $PORT
├── render.yaml # Render Blueprint (docker runtime, free plan, /health check)
├── tests/test_api.py # pytest suite against the app in-process
├── test_live.py # tests every endpoint of a running URL, pass/fail summary
└── test_api.sh # same walkthrough with plain curl
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
python train.py # -> model.pkluvicorn main:app --reload
# open http://127.0.0.1:8000/docscurl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{"MedInc": 8.3, "HouseAge": 41, "AveRooms": 6.98, "AveBedrms": 1.02,
"Population": 322, "AveOccup": 2.56, "Latitude": 37.88, "Longitude": -122.23}'
# {"predicted_price_usd": 408146.09, "predicted_value_100k": 4.0815}Run the tests: pytest -q tests
| Endpoint | Purpose |
|---|---|
GET / |
Service info and model metrics |
GET /health |
Liveness check (Render pings this) |
POST /predict |
Predict price for one block (8 numeric fields, validated) |
GET /metrics |
Request / error / prediction counters and average latency |
GET /dashboard |
Browser UI: one-click test run, live metric cards, prediction form, request log |
GET /docs |
Interactive Swagger UI |
docker build -t house-price-api .
docker run -p 8000:8000 house-price-apigit add -A && git commit -m "describe change" && git pushRender redeploys automatically on every push to main.
Option A, Blueprint (uses render.yaml):
- Render dashboard → New + → Blueprint → pick this repo → Apply.
Option B, manual Web Service:
- New + → Web Service → pick this repo.
- Runtime is auto-detected as Docker (because of the Dockerfile). Plan: Free.
- Health check path:
/health. Click Create Web Service.
Then open https://<your-service>.onrender.com/docs and try /predict.
The free instance sleeps after 15 min idle; the first request can take ~1 min.
main.py writes one JSON line per request to stdout, which Render shows in the
Logs tab of the service:
{"ts": "...", "level": "INFO", "event": "model_loaded", "path": "/app/model.pkl", "r2": 0.8486, ...}
{"ts": "...", "level": "INFO", "event": "prediction", "input": {...}, "predicted_price_usd": 408146.09}
{"ts": "...", "level": "INFO", "event": "request", "method": "POST", "path": "/predict", "status": 200, "latency_ms": 12.4}
GET /metrics returns live counters (requests, errors, predictions, average
latency, average predicted price) for a quick health overview.
Open http://localhost:8000/dashboard (or the same path on Render). Click Run all
tests to execute the full endpoint suite from the browser and see a pass/fail table
with latencies. The metric cards read /metrics, with optional 5-second auto-refresh,
and the form lets you try a prediction without curl.
Both scripts default to http://localhost:8000; pass a URL to test Render.
python test_live.py # 19 checks over /, /health, /predict, /metrics, /docs
python test_live.py https://<your-service>.onrender.com
./test_api.sh # curl version, prints raw responses