Module 5 of ML Zoomcamp hands you a frozen model and asks you to serve it. Sounds trivial until your probabilities are slightly off and nothing errors out.
Here is the setup that worked for me, with the actual code.
- Verify the artifact first Before touching anything, check you have the right model: sha256sum pipeline.bin
expected: 1646bbdcd38d4f044da6b630c5b332c93a314245a8c21929011c42de51f629f1
I learned this the hard way. Debugging numbers on the wrong artifact wastes an afternoon.
Pin the environment
uv sync --locked
That --locked flag is the whole story behind my probability mismatch. Without it, uv resolved a different sklearn build and everything shifted slightly. The reference env pins scikit-learn==1.7.2. Don't regenerate the lockfile for the graded run.Load the model the same way the API does
import pickle
from model import normalize_record
with open("pipeline.bin", "rb") as f:
pipeline = pickle.load(f) # DictVectorizer + LogisticRegression
lead = {
"lead_source": "paid_ads",
"industry": "technology",
"employment_status": "employed",
"location": "north_america",
"number_of_courses_viewed": 2,
"annual_income": 79276.0,
"interaction_count": 4,
"lead_score": 0.41,
}
record = normalize_record(lead)
prob = float(pipeline.predict_proba([record])[0, 1])
print(round(prob, 3)) # 0.533
The key line is normalize_record. It applies the training-set medians and "NA" for missing categoricals. Hand-rolling imputation gives you a different vector and a different answer.
- Serve it @app.post("/predict") def predict(lead: Lead): record = normalize_record(lead.model_dump()) prob = float(pipeline.predict_proba([record])[0, 1]) return {"conversion_probability": round(prob, 6), "conversion": prob >= 0.5} uv run uvicorn predict:app --host 0.0.0.0 --port 9696 curl -s http://localhost:9696/health # {"status":"ok","model_sha256":"1646...","feature_defaults_sha256":"1694..."} Note the API loads the pipeline once at startup. Swap the artifact, restart uvicorn.
- Containerize and confirm parity FROM python:3.11.15-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:0.10.11 /uv /uvx /bin/
ENV PATH="/code/.venv/bin:$PATH" \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
WORKDIR /code
COPY pyproject.toml uv.lock .python-version ./
RUN uv sync --locked --no-dev
COPY model.py predict.py feature_defaults.json model_metadata.json pipeline.bin ./
EXPOSE 9696
ENTRYPOINT ["uvicorn", "predict:app", "--host", "0.0.0.0", "--port", "9696"]
docker build -t zoomcamp-model:2026-hw5 .
docker run --rm -p 9696:9696 zoomcamp-model:2026-hw5
python q6_test.py
{'conversion_probability': 0.769799, 'conversion': True}
Same inference locally and in the container. If yours differ, the build context has a different artifact or lockfile. Free port 9696 before running (stop local uvicorn first).
What stuck with me
Serialize the whole pipeline, not just the model, so preprocessing travels with it. And verify before you debug. Most of my "model bugs" were environment bugs.
Full code, notebook and FAQ are here: https://github.com/marcos-Nieves-24/machine-learning-zoomcamp
Top comments (1)
A deployment checklist for frozen models should include the model artifact hash, preprocessing version, and a single known input/output fixture. Those three checks catch many cases where the container is healthy but serving a different model contract.