<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marcos Alvarez</title>
    <description>The latest articles on DEV Community by Marcos Alvarez (@marcos_alvarez_251b1536b2).</description>
    <link>https://dev.to/marcos_alvarez_251b1536b2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209610%2F40d58f1c-1079-4e0a-b632-d969409ab01d.webp</url>
      <title>DEV Community: Marcos Alvarez</title>
      <link>https://dev.to/marcos_alvarez_251b1536b2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcos_alvarez_251b1536b2"/>
    <language>en</language>
    <item>
      <title>Deploying a Frozen ML Model with FastAPI and Docker</title>
      <dc:creator>Marcos Alvarez</dc:creator>
      <pubDate>Mon, 26 Oct 2026 22:46:31 +0000</pubDate>
      <link>https://dev.to/marcos_alvarez_251b1536b2/deploying-a-frozen-ml-model-with-fastapi-and-docker-81k</link>
      <guid>https://dev.to/marcos_alvarez_251b1536b2/deploying-a-frozen-ml-model-with-fastapi-and-docker-81k</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here is the setup that worked for me, with the actual code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the artifact first
Before touching anything, check you have the right model:
sha256sum pipeline.bin&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  expected: 1646bbdcd38d4f044da6b630c5b332c93a314245a8c21929011c42de51f629f1
&lt;/h1&gt;

&lt;p&gt;I learned this the hard way. Debugging numbers on the wrong artifact wastes an afternoon.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Pin the environment&lt;br&gt;
uv sync --locked&lt;br&gt;
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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load the model the same way the API does&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;import pickle&lt;br&gt;
from model import normalize_record&lt;/p&gt;

&lt;p&gt;with open("pipeline.bin", "rb") as f:&lt;br&gt;
    pipeline = pickle.load(f)  # DictVectorizer + LogisticRegression&lt;/p&gt;

&lt;p&gt;lead = {&lt;br&gt;
    "lead_source": "paid_ads",&lt;br&gt;
    "industry": "technology",&lt;br&gt;
    "employment_status": "employed",&lt;br&gt;
    "location": "north_america",&lt;br&gt;
    "number_of_courses_viewed": 2,&lt;br&gt;
    "annual_income": 79276.0,&lt;br&gt;
    "interaction_count": 4,&lt;br&gt;
    "lead_score": 0.41,&lt;br&gt;
}&lt;br&gt;
record = normalize_record(lead)&lt;br&gt;
prob = float(pipeline.predict_proba([record])[0, 1])&lt;br&gt;
print(round(prob, 3))  # 0.533&lt;br&gt;
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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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 &amp;gt;= 0.5}
uv run uvicorn predict:app --host 0.0.0.0 --port 9696
curl -s &lt;a href="http://localhost:9696/health" rel="noopener noreferrer"&gt;http://localhost:9696/health&lt;/a&gt;
# {"status":"ok","model_sha256":"1646...","feature_defaults_sha256":"1694..."}
Note the API loads the pipeline once at startup. Swap the artifact, restart uvicorn.&lt;/li&gt;
&lt;li&gt;Containerize and confirm parity
FROM python:3.11.15-slim-bookworm&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;COPY --from=ghcr.io/astral-sh/uv:0.10.11 /uv /uvx /bin/&lt;/p&gt;

&lt;p&gt;ENV PATH="/code/.venv/bin:$PATH" \&lt;br&gt;
    UV_COMPILE_BYTECODE=1 \&lt;br&gt;
    UV_LINK_MODE=copy&lt;/p&gt;

&lt;p&gt;WORKDIR /code&lt;br&gt;
COPY pyproject.toml uv.lock .python-version ./&lt;br&gt;
RUN uv sync --locked --no-dev&lt;/p&gt;

&lt;p&gt;COPY model.py predict.py feature_defaults.json model_metadata.json pipeline.bin ./&lt;/p&gt;

&lt;p&gt;EXPOSE 9696&lt;br&gt;
ENTRYPOINT ["uvicorn", "predict:app", "--host", "0.0.0.0", "--port", "9696"]&lt;br&gt;
docker build -t zoomcamp-model:2026-hw5 .&lt;br&gt;
docker run --rm -p 9696:9696 zoomcamp-model:2026-hw5&lt;br&gt;
python q6_test.py&lt;/p&gt;

&lt;h1&gt;
  
  
  {'conversion_probability': 0.769799, 'conversion': True}
&lt;/h1&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;What stuck with me&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Full code, notebook and FAQ are here: &lt;a href="https://github.com/marcos-Nieves-24/machine-learning-zoomcamp" rel="noopener noreferrer"&gt;https://github.com/marcos-Nieves-24/machine-learning-zoomcamp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>zoomcamp</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
