Day 50: Save & Deploy Models (Streamlit, Flask)
✅ 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
Step 1: train and save
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))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
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"}))Step 3: a Flask API
# 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)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())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"}'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
# 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.")pip install streamlit streamlit run streamlit_app.py # opens http://localhost:8501
<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>Step 5: package and deploy
pandas==2.3.3 scikit-learn==1.9.1 joblib==1.6.0 flask==3.1.3 streamlit==1.40.0
| Option | Good for | Cost |
|---|---|---|
| Streamlit Community Cloud | Demo apps from a GitHub repo | Free |
| Hugging Face Spaces | Streamlit/Gradio demos for your portfolio | Free tier |
| Render / Railway | Flask/FastAPI APIs | Free tier, then paid |
| AWS / GCP / Azure | Company production, scaling, security | Pay as you go |
| Docker | Same environment everywhere | Free tool |
Step 6: monitor and retrain
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'}")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
Practice tasks
🏋️ Practice – do it yourself
predict_price(dict) function.Show solution
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")/batch endpoint to the Flask app that accepts a list of applicants and returns a list of probabilities.Quick quiz
🧠 Quick Quiz – test yourself
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!
Week 9: Day 51 – deep learning and neural networks explained, with a model you can train on a laptop.
