Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
⚙️ Week 8 – Advanced ML  ·  Day 50

Day 50: Save & Deploy Models (Streamlit, Flask)

⏱ 2.5 hours📶 Intermediate → Advanced🎯 100% self-study
Day 50 / 60

✅ Today you will learn

  • Save and load a full pipeline with joblib
  • Write a model card (version, metrics, features)
  • Serve predictions with a Flask JSON API
  • Build a Streamlit app in 30 lines
  • Deploy, monitor and retrain

🏢 In the real world

A model in a notebook helps nobody. The loan team wants a web form, the mobile app team wants an API, and management wants to know which version is live and how accurate it is. Today your model leaves the notebook.

From notebook to product

Train pipeline in notebookSave: loan_model.joblib + model_card.jsonServe: Flask API or Streamlit appUsers / mobile app call itMonitor and retrain
The deployment journey

Step 1: train and save

Python – train.py
import json
import joblib
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

l = pd.read_csv("loan_applications.csv")
NUM = ["monthly_income", "credit_score", "loan_amount", "years_employed"]
CAT = ["employment"]

def add_features(df):
    df = df.copy()
    df["lti"] = df["loan_amount"] / (df["monthly_income"] * 12)
    return df

X, y = add_features(l)[NUM + ["lti"] + CAT], l["approved"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
pipe = Pipeline([("prep", ColumnTransformer([("num", StandardScaler(), NUM + ["lti"]),
                                             ("cat", OneHotEncoder(handle_unknown="ignore"), CAT)])),
                 ("clf", LogisticRegression(C=1.0, max_iter=2000))]).fit(X_tr, y_tr)
auc = roc_auc_score(y_te, pipe.predict_proba(X_te)[:, 1])
joblib.dump(pipe, "loan_model.joblib")
card = {"model": "loan approval – logistic regression", "version": "1.0.0", "trained_rows": len(X_tr),
        "features": NUM + ["lti"] + CAT, "test_auc": round(auc, 3), "threshold": 0.5,
        "sklearn_version": sklearn.__version__}
json.dump(card, open("model_card.json", "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print("saved loan_model.joblib | test AUC", round(auc, 3))
print(json.dumps(card, indent=2, ensure_ascii=False))
saved loan_model.joblib | test AUC 0.915 { "model": "loan approval – logistic regression", "version": "1.0.0", "trained_rows": 960, "features": [ "monthly_income", "credit_score", "loan_amount", "years_employed", "lti", "employment" ], "test_auc": 0.915, "threshold": 0.5, "sklearn_version": "1.9.1" }
⚠️ Save the whole pipeline

Saving only the classifier forces the app to re-implement scaling and encoding – a classic production bug. Save the Pipeline so raw input goes in and a probability comes out. Load it with the same scikit-learn version (recorded in the model card).

Step 2: a clean prediction function

Python – predict.py
import joblib
import pandas as pd
model = joblib.load("loan_model.joblib")

def score(applicant: dict) -> dict:
    df = pd.DataFrame([applicant])
    df["lti"] = df["loan_amount"] / (df["monthly_income"] * 12)
    p = float(model.predict_proba(df)[0, 1])
    band = "auto-approve" if p >= 0.8 else "auto-reject" if p < 0.2 else "manual review"
    return {"approval_probability": round(p, 3), "decision": band}

print(score({"monthly_income": 65000, "credit_score": 790, "loan_amount": 500000, "years_employed": 7, "employment": "Salaried"}))
print(score({"monthly_income": 28000, "credit_score": 610, "loan_amount": 1200000, "years_employed": 1, "employment": "Business"}))
print(score({"monthly_income": 45000, "credit_score": 720, "loan_amount": 700000, "years_employed": 4, "employment": "Self-Employed"}))
{'approval_probability': 0.98, 'decision': 'auto-approve'} {'approval_probability': 0.001, 'decision': 'auto-reject'} {'approval_probability': 0.521, 'decision': 'manual review'}

Step 3: a Flask API

Python – app.py
# app.py – a JSON prediction API
import joblib
import pandas as pd
from flask import Flask, request, jsonify

app = Flask(__name__)
model = joblib.load("loan_model.joblib")
REQUIRED = ["monthly_income", "credit_score", "loan_amount", "years_employed", "employment"]

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict")
def predict():
    data = request.get_json(silent=True) or {}
    missing = [f for f in REQUIRED if f not in data]
    if missing:
        return jsonify(error=f"missing fields: {missing}"), 400
    if not 300 <= float(data["credit_score"]) <= 900:
        return jsonify(error="credit_score must be 300–900"), 400
    df = pd.DataFrame([data])
    df["lti"] = df["loan_amount"] / (df["monthly_income"] * 12)
    p = float(model.predict_proba(df)[0, 1])
    return jsonify(approval_probability=round(p, 3), approved=p >= 0.5)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Python – test the API
FLASK_APP_SOURCE = """# app.py – a JSON prediction API
import joblib
import pandas as pd
from flask import Flask, request, jsonify

app = Flask(__name__)
model = joblib.load("loan_model.joblib")
REQUIRED = ["monthly_income", "credit_score", "loan_amount", "years_employed", "employment"]

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict")
def predict():
    data = request.get_json(silent=True) or {}
    missing = [f for f in REQUIRED if f not in data]
    if missing:
        return jsonify(error=f"missing fields: {missing}"), 400
    if not 300 <= float(data["credit_score"]) <= 900:
        return jsonify(error="credit_score must be 300–900"), 400
    df = pd.DataFrame([data])
    df["lti"] = df["loan_amount"] / (df["monthly_income"] * 12)
    p = float(model.predict_proba(df)[0, 1])
    return jsonify(approval_probability=round(p, 3), approved=p >= 0.5)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
"""
open("app.py", "w").write(FLASK_APP_SOURCE)
import importlib, sys
sys.path.insert(0, ".")
app = importlib.import_module("app").app
client = app.test_client()                         # call the API without starting a server
print(client.get("/health").get_json())
r = client.post("/predict", json={"monthly_income": 65000, "credit_score": 790, "loan_amount": 500000,
                                  "years_employed": 7, "employment": "Salaried"})
print(r.status_code, r.get_json())
r = client.post("/predict", json={"monthly_income": 65000})
print(r.status_code, r.get_json())
{'status': 'ok'} 200 {'approval_probability': 0.98, 'approved': True} 400 {'error': "missing fields: ['credit_score', 'loan_amount', 'years_employed', 'employment']"}
Terminal
pip install flask joblib scikit-learn pandas
python app.py
# in another terminal:
curl -X POST http://localhost:5000/predict -H "Content-Type: application/json" \
     -d '{"monthly_income": 65000, "credit_score": 790, "loan_amount": 500000, "years_employed": 7, "employment": "Salaried"}'
💡 Validate inputs

Real users send empty fields, text instead of numbers and impossible values. Return a clear 400 error instead of crashing. FastAPI with Pydantic does this validation automatically – a great next step.

Step 4: a Streamlit web app

Python – streamlit_app.py
# streamlit_app.py  –  run with:  streamlit run streamlit_app.py
import joblib
import pandas as pd
import streamlit as st

st.set_page_config(page_title="Instant Loan Check", page_icon="💳")
model = joblib.load("loan_model.joblib")

st.title("💳 Instant Loan Eligibility")
st.caption("Demo model trained on practice data – not financial advice.")
col1, col2 = st.columns(2)
income = col1.number_input("Monthly income (₹)", 10000, 500000, 50000, step=1000)
loan = col2.number_input("Loan amount (₹)", 50000, 5000000, 600000, step=10000)
score = col1.slider("Credit score", 300, 900, 720)
years = col2.slider("Years employed", 0, 40, 4)
emp = st.selectbox("Employment", ["Salaried", "Self-Employed", "Business"])

if st.button("Check eligibility"):
    df = pd.DataFrame([{"monthly_income": income, "credit_score": score, "loan_amount": loan,
                        "years_employed": years, "employment": emp}])
    df["lti"] = df["loan_amount"] / (df["monthly_income"] * 12)
    p = model.predict_proba(df)[0, 1]
    st.metric("Approval probability", f"{p:.0%}")
    st.progress(float(p))
    if p >= 0.5:
        st.success("Likely to be approved 🎉")
    else:
        st.warning(f"Unlikely right now. Loan is {df['lti'][0]:.1f}× your yearly income – try a smaller amount.")
Terminal
pip install streamlit
streamlit run streamlit_app.py      # opens http://localhost:8501
HTML
<div style="font-family:system-ui,sans-serif;max-width:420px;margin:12px auto;padding:18px;border:1px solid #e5e7eb;border-radius:14px">
<h3 style="margin:0 0 4px">💳 Instant Loan Eligibility</h3><small style="color:#6b7280">Demo model trained on practice data</small>
<label style="display:block;margin-top:12px">Monthly income (₹)<input id="inc" type="number" value="50000" style="width:100%;padding:6px"></label>
<label style="display:block;margin-top:8px">Loan amount (₹)<input id="loan" type="number" value="600000" style="width:100%;padding:6px"></label>
<label style="display:block;margin-top:8px">Credit score: <b id="sv">720</b><input id="sc" type="range" min="300" max="900" value="720" style="width:100%" oninput="sv.textContent=this.value"></label>
<button onclick="go()" style="margin-top:12px;background:#ff4b4b;color:#fff;border:0;padding:8px 14px;border-radius:8px">Check eligibility</button>
<div id="out" style="margin-top:12px;font-size:18px"></div></div>
<script>function go(){var i=+inc.value,l=+loan.value,s=+sc.value,lti=l/(i*12);
var z=-0.4+0.035*(s-700)-1.6*(lti-1.2);var p=1/(1+Math.exp(-z));
out.innerHTML='Approval probability: <b>'+Math.round(p*100)+'%</b><br>'+(p>=0.5?'<span style="color:#16a34a">Likely approved 🎉</span>':'<span style="color:#b45309">Unlikely – loan is '+lti.toFixed(1)+'× yearly income</span>');}</script>
▶  What the Streamlit app looks like (simplified browser demo)

Step 5: package and deploy

requirements.txt
pandas==2.3.3
scikit-learn==1.9.1
joblib==1.6.0
flask==3.1.3
streamlit==1.40.0
OptionGood forCost
Streamlit Community CloudDemo apps from a GitHub repoFree
Hugging Face SpacesStreamlit/Gradio demos for your portfolioFree tier
Render / RailwayFlask/FastAPI APIsFree tier, then paid
AWS / GCP / AzureCompany production, scaling, securityPay as you go
DockerSame environment everywhereFree tool

Step 6: monitor and retrain

Python
import numpy as np
import pandas as pd
train = pd.read_csv("loan_applications.csv")
rng = np.random.default_rng(1)
live = train.sample(300, random_state=1).copy()
live["monthly_income"] = live["monthly_income"] * rng.normal(1.35, 0.05, len(live))   # incomes rose ~35%
live["credit_score"] = live["credit_score"] - 30                                      # riskier applicants
def psi(expected, actual, bins=10):
    edges = np.quantile(expected, np.linspace(0, 1, bins + 1)); edges[0], edges[-1] = -np.inf, np.inf
    e = np.histogram(expected, edges)[0] / len(expected); a = np.histogram(actual, edges)[0] / len(actual)
    e, a = np.clip(e, 1e-4, None), np.clip(a, 1e-4, None)
    return float(np.sum((a - e) * np.log(a / e)))
for col in ["monthly_income", "credit_score", "years_employed"]:
    v = psi(train[col], live[col])
    print(f"{col:15s} PSI = {v:.3f}  → {'ALERT: drift' if v > 0.2 else 'watch' if v > 0.1 else 'stable'}")
monthly_income PSI = 0.348 → ALERT: drift credit_score PSI = 0.207 → ALERT: drift years_employed PSI = 0.027 → stable
🧠 Models get stale

Customer behaviour, prices and the economy change, so the data the model sees drifts away from its training data. Log every prediction, track input drift (PSI) and real outcomes, and retrain on a schedule or when performance drops.

🏅 Job skills unlocked today

✔ Persist pipelines with joblib and model cards✔ Build and test a Flask prediction API✔ Create a Streamlit app✔ Plan deployment, monitoring and retraining

Practice tasks

🏋️ Practice – do it yourself

Task 1: Save the house-price pipeline from Day 45 with joblib and write a predict_price(dict) function.
Show solution
Python
import joblib, pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.linear_model import Ridge
h = pd.read_csv("house_prices.csv"); num = ["area_sqft", "bedrooms", "bathrooms", "age_years", "metro_km"]; cat = ["city", "furnishing", "parking"]
pipe = Pipeline([("p", ColumnTransformer([("n", StandardScaler(), num), ("c", OneHotEncoder(handle_unknown="ignore"), cat)])), ("m", Ridge())]).fit(h[num + cat], h["price_lakh"])
joblib.dump(pipe, "house_model.joblib")
def predict_price(d):
    return round(float(joblib.load("house_model.joblib").predict(pd.DataFrame([d]))[0]), 1)
print(predict_price({"area_sqft": 1100, "bedrooms": 2, "bathrooms": 2, "age_years": 5, "metro_km": 1.5, "city": "Hyderabad", "furnishing": "Semi-Furnished", "parking": "Yes"}), "lakh")
65.0 lakh
Task 2: Add a /batch endpoint to the Flask app that accepts a list of applicants and returns a list of probabilities.
Task 3: Deploy the Streamlit app for free on Streamlit Community Cloud from a GitHub repo and add the link to your CV.

Quick quiz

🧠 Quick Quiz – test yourself

1. What should you save for deployment?
Raw input → prediction in one object.
2. An API returns HTTP 400 when…
Client error – tell the caller what to fix.
3. Streamlit is best for…
No HTML/JS needed.
4. Data drift means…
Monitor with PSI or distribution checks.
5. Why record the scikit-learn version in the model card?
Pin versions in requirements.txt.

Interview corner

💼 Interview Corner

How would you deploy a machine-learning model?

Train a full pipeline, version it with a model card, wrap it in an API (Flask/FastAPI) with input validation and tests, containerise with Docker, deploy to a cloud service, log predictions, monitor drift and performance, and retrain on a schedule or trigger.

Batch vs real-time prediction?

Batch scores many records on a schedule (nightly churn scores to CRM) – simple and cheap. Real-time scores one request instantly through an API (loan decision in an app) – needs low latency and high availability.

Summary

  • joblib.dump(pipeline) + a model card = a deployable artefact.
  • Flask/FastAPI for APIs, Streamlit for quick apps.
  • Validate inputs, pin versions, test with a client.
  • Monitor drift and outcomes; retrain regularly.
  • Week 8 done – your models now reach real users!
➡️ Next

Week 9: Day 51 – deep learning and neural networks explained, with a model you can train on a laptop.

Frequently Asked Questions

pickle or joblib?
joblib is the scikit-learn recommendation for models with large NumPy arrays. Never load pickle/joblib files from untrusted sources – they can run code.
Flask or FastAPI?
Flask is simple and everywhere. FastAPI is faster, validates inputs with type hints and auto-creates API docs – increasingly popular for ML APIs.
← Data Science (60 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicDay 51: Deep Learning & Neural Networks Your progress: 50 / 60 (83%) · Lesson 51 / 60 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free