The Course 3 champion model behind a FastAPI service, a PostgreSQL database, a Streamlit underwriting portal, containers, and a CI/CD pipeline: an end-to-end, tested, deployable decision-support system.
rating_platform package with one module per concernCompanion notebook: lesson-08b-capstone.ipynb — prototype each stage with self-checking exercises, then move it into the package.
rating_platform/
├── pyproject.toml # extras: portal, train, postgres
├── uv.lock
├── Dockerfile # targets: api, portal
├── compose.yaml # db, init, api, portal
├── .dockerignore .env.example
├── .github/workflows/ci.yml
├── models/champion/ # model.joblib + manifest.json
├── src/rating_platform/
│ ├── settings.py schemas.py manual.py
│ ├── model.py rating.py money.py
│ ├── db.py api.py client.py
│ ├── portfolio.py monitoring.py
│ ├── dashboard.py cli.py
│ └── manuals/motor_2025.toml
└── tests/ # 48 tests, fixtures in tests/data
| Module | Lines | From |
|---|---|---|
dashboard.py | 239 | Lesson 5 |
api.py | 185 | Lesson 4 |
model.py | 171 | Course 3 + 8A |
rating.py | 141 | Lessons 1, 4 |
db.py | 127 | Lesson 6 |
cli.py | 89 | all |
schemas.py | 83 | Lessons 1, 4 |
portfolio.py | 72 | Lesson 5 |
manual.py, settings.py, monitoring.py, … | 176 | 1, 7, 8A |
Each one working and tested before the next.
$ uv run rating-platform train
saved glm-dcbe5b5dbb to models/champion
holdout_year: 2025
holdout_actual_to_expected: 1.0143
holdout_gini: 0.2753
trained_rows: 84346
trained_car_years: 72,780.5092
trained_claims: 4565
model.joblib (a few KB) and manifest.json are committed: the artifact the API serves is reviewed like codeTraining reads Parquet (the train extra); the API image doesn’t train, so it doesn’t carry pyarrow.
def build_engine(manual, model, liability_basis,
trend_factor=1.0, target_loss_ratio=0.68):
if liability_basis == "model":
liability = ModelLiability(model, trend_factor,
target_loss_ratio)
else:
liability = ManualLiability(manual)
return RatingEngine({"LIAB": liability,
"COLL": ManualCollision(manual),
"COMP": ManualComprehensive(manual)},
liability_basis)
$ uv run rating-platform init-db
loaded 18,249 vehicles into sqlite
(expected losses from glm-dcbe5b5dbb)
Liability from the model (pure premium × 1.041.5 ÷ 0.68), collision and comprehensive from the manual; RP_LIABILITY_BASIS=manual switches back without a code change. init-db scores the 2025 book with the champion and loads it in one transaction.
| Endpoint | Does |
|---|---|
POST /quotes | validate, price, store, return (201) |
POST /quotes/batch | up to 1,000 in one transaction, one model call |
GET /quotes/{id} | a stored quote (404 if none) |
GET /health | status, model version, database |
GET /model | the manifest |
GET /monitoring/drift | PSI of recent quotes vs the book |
quote requests: 963 valid, 37 rejected before
reaching the API
batch of 963: 0.20 s, total premium 1,577,722.41
$ docker compose up --build -d
$ curl -s localhost:8000/health
{"status":"ok","model_version":"glm-dcbe5b5dbb",
"database":"postgresql"}
$ curl -s -H "X-API-Key: $KEY" \
"localhost:8000/monitoring/drift?last=963"
{"quotes": 963, "reference_vehicles": 18249,
"features": [
{"feature": "driver_age", "psi": 0.0061,
"status": "stable"}, ...]}
| Check | Result |
|---|---|
| Tests | 48 passed, 96% coverage |
| ruff, mypy --strict | clean |
| Images | API 521 MB, portal 807 MB, non-root |
| Workflow | actionlint clean; smoke test passes locally |
| Start to healthy | 2.2 s |
| Area | Evidence |
|---|---|
| Correctness | boundaries, contract, transactions, model integrity; ≥ 90% |
| Model | manifest with holdout A/E and Gini; refuses tampering |
| Service | 201 / 401 / 404 / 422; OpenAPI page; vectorized batch |
| Data | quotes in PostgreSQL with the model version; UTC timestamps |
| Portal | proposal numbers match the answer key; AppTest tests |
| Operations | one-command start; no secrets in images or Git; drift endpoint; CI (checks → images → smoke test) green |
Add a one-page README for a reviewer: what it does, how to run it, how the model was validated, what you monitor.
Extensions: PostgreSQL in CI; rate limits; a registry with version tags; a shadow-scored challenger.
| Holdout A/E · Gini | 1.0143 · 0.2753 |
| Training rows · claims | 84,346 · 4,565 |
| Base risk quote (Sedan, 45, 12,000 mi, $30k, $500s) | LIAB 726.73 · COLL 576.85 · COMP 158.35 |
| 22, Sports, urban, 16,000 mi, $45k | LIAB 4,648.59 · total 7,008.88 |
| Quote requests: valid · rejected | 963 · 37 |
| Total premium, 963 quotes | $1,577,722.41 |
| Book loaded | 18,249 vehicles |
| Current loss ratio · margin | 77.7% · −2.7% |
| Proposal: premium · loss ratio | +9.2% · 71.2% |
| Proposal: vehicles up > 15% | 10,902 |
| Driver-age PSI, new business | 0.0061 |
| After the campaign | 0.6742 (investigate) |
Claims data at scale, stochastic reserving, experience studies, Bayesian credibility, extreme values and capital, deep learning, and the governance that makes advanced models usable.