A practical journey from training a machine learning model in a notebook to exposing it as an API and connecting it to a real user interface.
Building a machine learning model is an important achievement. But training a model is not the end of the machine learning lifecycle.
For many people learning Data Science and Machine Learning, the journey often looks like this:
- Collect a dataset
- Clean the data
- Perform exploratory data analysis
- Engineer features
- Train several models
- Evaluate their performance
- Select the best model
- Generate predictions
- Save the notebook
At that point, the model works. But only inside your environment. A real-world machine learning system needs to go further.
The model needs to become accessible to:
- Web applications
- Mobile applications
- Internal business systems
- Other developers
- Data platforms
- Customer-facing applications
This is where Machine Learning Model Deployment becomes important.
In our upcoming FastAPI Fundamentals and Deploying Machine Learning Models with FastAPI Workshop, we will explore how to take a trained machine learning model and expose it through a production-style REST API using Python and FastAPI.
The goal is not simply to train better models, the goal is to build machine learning systems that people and applications can actually use. Move machine learning beyond the notebook and make it usable in real applications.
What We Will Build.
By the end of the workshop, we will deploy a Customer Churn Prediction Model and connect it to a ready-made web interface.
The user interface will already be developed and made available to all participants.
This allows us to concentrate on the most important part of the workshop: building the machine learning backend and exposing the model through API endpoints.
Our workflow will therefore focus on:
- Understanding APIs
- Understanding FastAPI
- Building API endpoints
- Loading a trained machine learning model
- Validating incoming customer data
- Sending the data to the model
- Generating churn probabilities
- Returning predictions as JSON
- Connecting the FastAPI API to the frontend UI
When everything is connected, a user will be able to enter customer information into the interface and receive a result such as:
prediction_response = {
"prediction": "Likely to Churn",
"churn_probability": 0.81
}
Instead of seeing:
prediction = 1
The end user could see something meaningful:
result = {
"prediction": "Likely to Churn",
"risk_level": "High",
"churn_probability": "81%",
"recommendation": "Consider retention outreach or a discount offer."
}
That is the transition from a machine learning experiment to a machine learning application.
The Problem: Your Model Works, But Only on Your Computer.
Imagine that you have spent several hours building a machine learning model.
You have successfully:
- Collected your dataset
- Cleaned and transformed the data
- Performed exploratory data analysis
- Selected important features
- Split the data into training and testing sets
- Trained several machine learning algorithms
- Evaluated their performance
- Selected the best-performing model
Eventually, you run:
prediction = model.predict(new_data)
print(prediction)
And the model returns:
prediction = [1]
Great.
Your machine learning model works. But there is still a problem.
Who can actually use it?
At this point, probably only someone who has:
- Your Jupyter Notebook
- Python installed
- Your dependencies installed
- Access to the trained model
- Knowledge of your preprocessing steps
- Knowledge of your Python code
That is not practical for a real application. Consider a telecom company using your churn model.
A customer service officer should not need to open Jupyter Notebook and run:
model.predict(customer_data)
A web application should not need to understand how Random Forest works. A mobile application should not need access to your notebook. A CRM should not need your entire machine learning project.
Instead, these applications should simply send data to your model and receive a prediction.
This is where APIs become extremely useful.
What Is an API?
API stands for:
- Application Programming Interface
An API provides a structured way for software applications to communicate with each other.
Think of an API as an intermediary. One application sends a request. Another application processes that request. A response is returned.
For our machine learning system, the communication might look like this:
Frontend
↓
API
↓
Machine Learning Model
↓
Prediction
↓
API
↓
Frontend
Suppose our Customer Churn Prediction system exposes this endpoint:
endpoint = "/predict"
The frontend sends customer information to the endpoint.
For example:
customer_data = {
"tenure": 12,
"monthly_charges": 89.50,
"contract": "Month-to-month",
"internet_service": "Fiber optic",
"payment_method": "Electronic check",
"paperless_billing": "Yes",
"senior_citizen": "No",
"dependents": "No"
}
The API receives the information. It then passes the customer data to the machine learning model.
The model calculates a prediction. The API returns:
response = {
"prediction": "Likely to Churn",
"churn_probability": 0.81
}
The frontend can now display:
The application using our API does not need to understand how our model was trained.
It does not need to know whether we used:
- Logistic Regression
- Random Forest
- XGBoost
- Gradient Boosting
- Neural Networks
It only needs to understand the API contract:
api_contract = {
"endpoint": "/predict",
"method": "POST",
"input": "Customer information",
"output": "Churn prediction"
}
This separation is extremely powerful.
Why FastAPI?
For this workshop, we will use FastAPI. FastAPI is a modern Python framework designed for building APIs.
It is particularly attractive for machine learning applications because most machine learning development already happens inside the Python ecosystem.
A Data Scientist or Machine Learning Engineer may already be working with:
- Pandas
- NumPy
- Scikit-learn
- XGBoost
- PyTorch
- TensorFlow
- Joblib
Adding FastAPI means we can build the serving layer without abandoning Python.
A very simple FastAPI application can look like this:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def home():
return {
"message": "Machine Learning API is running."
}
We can make our application slightly more descriptive:
from fastapi import FastAPI
app = FastAPI(
title="Customer Churn Prediction API",
description="Machine Learning API for predicting customer churn.",
version="1.0.0"
)
@app.get("/")
def home():
return {
"status": "healthy",
"message": "Customer Churn Prediction API is running."
}
Already, we have created a working API.
The Journey From Notebook to Production.
Our workshop will follow this architecture:
Raw Data
↓
Data Cleaning
↓
Feature Engineering
↓
Model Training
↓
Model Evaluation
↓
Model Pipeline
↓
Save Model
↓
FastAPI Application
↓
Prediction Endpoint
↓
Frontend
↓
User
In Python, we could represent the lifecycle as:
machine_learning_lifecycle = [
"Data Collection",
"Data Cleaning",
"Exploratory Data Analysis",
"Feature Engineering",
"Model Training",
"Model Evaluation",
"Model Persistence",
"API Development",
"Model Serving",
"Frontend Integration"
]
The important idea is that machine learning does not exist in isolation.
Eventually, it must interact with other software systems.
Our Business Problem: Customer Churn.
For the workshop, our main project will be Customer Churn Prediction. Customer churn occurs when a customer stops using a company's service.
Examples include customers:
- Cancelling a telecommunications subscription
- Closing a bank account
- Cancelling an insurance policy
- Leaving a streaming platform
- Cancelling a SaaS subscription
- Moving to another internet provider
For many companies, customer retention is extremely important.
Our machine learning problem can therefore be stated as:
Given information about a customer and their subscription behaviour, can we estimate their likelihood of leaving the company?
Our model might consider features such as:
- Customer tenure
- Monthly charges
- Contract type
- Internet service
- Payment method
- Paperless billing
- Senior citizen status
- Dependents
- Services subscribed to
Our target might be:
target = "churn"
Where:
churn_labels = {
0: "Not Likely to Churn",
1: "Likely to Churn"
}
This makes Customer Churn a binary classification problem.
Step 1: Prepare the Data.
Before training the model, we separate our features and target.
For example:
target_column = "Churn"
X = df.drop(columns=[target_column])
y = df[target_column]
We then divide the dataset into training and testing sets.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y
)
Why do we split the data?
Because we want to evaluate the model using observations it did not see during training.
Step 2: Build a Preprocessing Pipeline.
Real-world datasets rarely contain only perfectly formatted numerical values.
Our churn dataset may contain:
Numerical features
numerical_features = [
"tenure",
"monthly_charges"
]
and categorical features:
categorical_features = [
"contract",
"internet_service",
"payment_method",
"paperless_billing",
"senior_citizen",
"dependents"
]
We therefore need preprocessing.
Instead of manually repeating preprocessing every time we generate a prediction, we can build a reusable Scikit-learn pipeline.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
numerical_transformer = Pipeline(
steps=[
(
"scaler",
StandardScaler()
)
]
)
categorical_transformer = Pipeline(
steps=[
(
"encoder",
OneHotEncoder(
handle_unknown="ignore"
)
)
]
)
preprocessor = ColumnTransformer(
transformers=[
(
"numerical",
numerical_transformer,
numerical_features
),
(
"categorical",
categorical_transformer,
categorical_features
)
]
)
This is an important production concept.
The same preprocessing transformations used during training should also be used when new requests arrive through our API.
Step 3: Train the Machine Learning Model.
We can combine our preprocessing and classification algorithm into one pipeline.
For example:
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
classifier = RandomForestClassifier(
n_estimators=200,
random_state=42,
class_weight="balanced"
)
model_pipeline = Pipeline(
steps=[
(
"preprocessor",
preprocessor
),
(
"classifier",
classifier
)
]
)
Now we train the complete pipeline.
model_pipeline.fit(
X_train,
y_train
)
Notice what we are doing here.
We are not saving only the classifier.
We are creating:
Raw Customer Data
↓
Preprocessing
↓
Feature Transformation
↓
Random Forest
↓
Prediction
as one reusable pipeline.
Step 4: Evaluate the Model.
Training a model is not enough.
We need to understand how well it performs.
from sklearn.metrics import (
accuracy_score,
precision_score,
recall_score,
f1_score
)
y_pred = model_pipeline.predict(
X_test
)
evaluation = {
"accuracy": accuracy_score(
y_test,
y_pred
),
"precision": precision_score(
y_test,
y_pred
),
"recall": recall_score(
y_test,
y_pred
),
"f1_score": f1_score(
y_test,
y_pred
)
}
print(evaluation)
For churn prediction, we should not look at accuracy alone.
Imagine we care about identifying customers who are genuinely at risk of leaving.
In that situation, recall becomes particularly important.
It helps us answer:
Of all the customers who actually churned, how many did our model identify?
Machine learning metrics should therefore always be interpreted in the context of the business problem.
Step 5: Save the Complete Model Pipeline.
Once we are satisfied with our model, we save it.
We can use Joblib.
import joblib
MODEL_PATH = "model/customer_churn_pipeline.joblib"
joblib.dump(
model_pipeline,
MODEL_PATH
)
Now we have a reusable machine learning artifact.
The important distinction is:
Jupyter Notebook
Used for:
- Exploration
- Experimentation
- Model development
- Training
- Evaluation
Saved Model
Used for:
- Prediction
- Serving
- Applications
- Production systems
Our API does not need to retrain the model for every request.
It simply loads the trained model.
Step 6: Organise the Project.
A professional project should have a clear structure.
For example:
project_structure = """
customer-churn-fastapi/
│
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── schemas.py
│ ├── model_loader.py
│ └── predictor.py
│
├── data/
│ └── customer_churn.csv
│
├── model/
│ └── customer_churn_pipeline.joblib
│
├── notebooks/
│ └── model_training.ipynb
│
├── tests/
│ └── test_api.py
│
├── requirements.txt
├── README.md
└── .gitignore
"""
Each component has a responsibility.
The notebook trains the model.
The model directory stores the trained artifact.
The application directory contains the API.
The tests directory contains API tests.
This separation makes the project easier to understand and maintain.
Step 7: Load the Model Into FastAPI
We can create a small model loader.
from pathlib import Path
import joblib
MODEL_PATH = (
Path(__file__).resolve().parent.parent
/ "model"
/ "customer_churn_pipeline.joblib"
)
def load_model():
model = joblib.load(
MODEL_PATH
)
return model
Our API can now reuse the model without retraining it.
Step 8: Define the API Input Schema
When the frontend sends information, our API should know exactly what fields to expect.
We can define this using Pydantic.
from pydantic import BaseModel, Field
class CustomerInput(BaseModel):
tenure: int = Field(
...,
ge=0,
description="Number of months the customer has stayed."
)
monthly_charges: float = Field(
...,
ge=0,
description="Customer monthly charges."
)
contract: str
internet_service: str
payment_method: str
paperless_billing: str
senior_citizen: str
dependents: str
This gives our API an explicit data contract.
If someone sends:
invalid_request = {
"tenure": "twelve months",
"monthly_charges": "expensive"
}
FastAPI can reject the request before invalid data reaches the machine learning model.
Input validation is therefore not simply convenient.
It is part of building reliable systems.
Step 9: Create the Prediction Endpoint
Now we arrive at the central part of the workshop.
We connect FastAPI to our trained model.
import pandas as pd
from fastapi import FastAPI
from app.model_loader import load_model
from app.schemas import CustomerInput
app = FastAPI(
title="Customer Churn Prediction API",
description=(
"REST API for serving predictions "
"from a trained customer churn model."
),
version="1.0.0"
)
model = load_model()
@app.get("/")
def home():
return {
"status": "healthy",
"service": "Customer Churn Prediction API"
}
@app.post("/predict")
def predict_churn(
customer: CustomerInput
):
input_data = pd.DataFrame(
[
customer.model_dump()
]
)
prediction = model.predict(
input_data
)[0]
probabilities = model.predict_proba(
input_data
)[0]
churn_probability = probabilities[1]
prediction_label = (
"Likely to Churn"
if prediction == 1
else "Not Likely to Churn"
)
return {
"prediction": prediction_label,
"churn_probability": round(
float(churn_probability),
4
)
}
Now our machine learning model is accessible through:
prediction_endpoint = "POST /predict"
This is a major transition.
Before:
Notebook → model.predict()
Now:
Application → API → Model → Prediction → Application
Our model has become a service.
Step 10: Run the FastAPI Application
We can start the API using Uvicorn.
command = "uvicorn app.main:app --reload"
The development server will typically become available at:
api_url = "http://127.0.0.1:8000"
FastAPI also automatically generates interactive API documentation.
swagger_docs = "http://127.0.0.1:8000/docs"
This means that before connecting our UI, we can test the machine learning API directly from our browser.
Step 11: Send a Prediction Request.
Our frontend might eventually send information similar to:
customer = {
"tenure": 12,
"monthly_charges": 89.50,
"contract": "Month-to-month",
"internet_service": "Fiber optic",
"payment_method": "Electronic check",
"paperless_billing": "Yes",
"senior_citizen": "No",
"dependents": "No"
}
The request is sent to:
endpoint = "POST /predict"
Our FastAPI backend sends the customer information through:
prediction = model.predict(
input_data
)
and:
probability = model.predict_proba(
input_data
)
The API returns:
response = {
"prediction": "Likely to Churn",
"churn_probability": 0.81
}
The frontend then converts this machine-readable response into something useful for a human.
For example:
interface_result = {
"status": "Likely to Churn",
"churn_probability": "81%",
"risk": "High",
"recommendation": (
"Consider retention outreach "
"or a discount offer."
)
}
From Model Output to Business Decision.
This is an important distinction. A machine learning model might technically return:
raw_prediction = 1
But 1 means very little to a business user.
We therefore transform the prediction into something understandable:
business_prediction = {
"customer_status": "Likely to Churn",
"probability": "81%",
"risk_level": "High"
}
We can go further and connect it to a business action:
business_action = {
"risk_level": "High",
"recommended_action": (
"Prioritise customer for retention outreach."
)
}
This is ultimately why machine learning systems are built.
Not simply to produce numbers.
But to support decisions.
Connecting FastAPI to the User Interface
The final architecture will look like this:
USER
↓
CUSTOMER CHURN UI
↓
POST /predict
↓
FASTAPI
↓
PYDANTIC VALIDATION
↓
PREPROCESSING PIPELINE
↓
MACHINE LEARNING MODEL
↓
CHURN PROBABILITY
↓
JSON RESPONSE
↓
CUSTOMER CHURN UI
↓
USER
The frontend and machine learning model are therefore separated.
The frontend is responsible for presentation.
FastAPI is responsible for communication and serving.
The machine learning pipeline is responsible for prediction.
Separation of Responsibilities
A clean architecture might look conceptually like this:
system_components = {
"frontend": {
"responsibility": (
"Collect user input and display results"
)
},
"fastapi": {
"responsibility": (
"Receive requests, validate input "
"and return responses"
)
},
"machine_learning_pipeline": {
"responsibility": (
"Transform data and generate predictions"
)
}
}
This separation is one of the principles behind maintainable production systems.
Testing the API
A professional API should also be testable.
For example:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(
app
)
def test_health_endpoint():
response = client.get("/")
assert response.status_code == 200
def test_prediction_endpoint():
customer = {
"tenure": 12,
"monthly_charges": 89.50,
"contract": "Month-to-month",
"internet_service": "Fiber optic",
"payment_method": "Electronic check",
"paperless_billing": "Yes",
"senior_citizen": "No",
"dependents": "No"
}
response = client.post(
"/predict",
json=customer
)
assert response.status_code == 200
result = response.json()
assert "prediction" in result
assert "churn_probability" in result
This allows us to verify that our API continues to behave correctly as the application evolves.
What Makes This Different From Running a Notebook?
Inside a notebook:
prediction = model.predict(
customer_data
)
Only your Python environment can easily interact with the model.
After deployment:
application = "Any authorised client"
endpoint = "/predict"
response = "JSON"
Now the model can potentially serve:
- Web applications
- Mobile applications
- CRM systems
- Internal company applications
- Other APIs
- Data platforms
That is why deployment is such an important machine learning skill.
Model Development vs Model Serving
It is useful to distinguish between two separate activities.
Model Development
model_development = [
"Explore data",
"Clean data",
"Engineer features",
"Experiment",
"Train models",
"Evaluate models",
"Select model"
]
Model Serving
model_serving = [
"Load trained model",
"Receive request",
"Validate input",
"Transform data",
"Generate prediction",
"Return response",
"Serve application"
]
FastAPI lives primarily in the serving layer.
This is the bridge between machine learning and software engineering.
The Bigger Picture
The eventual production architecture may become much larger.
For example:
production_architecture = {
"client": "Web or mobile application",
"api_gateway": "API access layer",
"ml_api": "FastAPI prediction service",
"model": "Customer churn pipeline",
"database": "Prediction and customer records",
"monitoring": "Performance and health monitoring",
"logging": "Application and prediction logs"
}
A more advanced system might eventually include:
- Docker
- PostgreSQL
- Authentication
- Cloud deployment
- CI/CD
- Monitoring
- Model versioning
- Logging
- Drift detection
- Retraining pipelines
- Kubernetes
- MLOps platforms
But those systems all build on the same fundamental concept we are learning:
How do we make a trained machine learning model accessible to another application?
What Participants Will Learn.
By the end of the workshop, participants should understand:
Python Foundations
- Functions
- Classes
- Decorators
- How these concepts appear inside FastAPI
API Fundamentals
- What an API is
- Why APIs are important
- Requests and responses
- GET requests
- POST requests
- JSON
- API endpoints
FastAPI.
- Creating a FastAPI application
- Defining routes
- Creating request models
- Using Pydantic
- Input validation
- Returning responses
- Interactive Swagger documentation
Machine Learning Deployment
- Saving trained models
- Loading trained models
- Using Scikit-learn pipelines
- Serving predictions
- Returning prediction probabilities
- Creating
/predict
Integration
- Sending UI data to FastAPI
- Receiving prediction responses
- Displaying model predictions
- Understanding frontend/backend separation
What You Should Be Able to Explain After the Workshop
By the end, you should be able to explain this entire lifecycle:
Jupyter Notebook
↓
Machine Learning Pipeline
↓
Trained Model
↓
Saved Model Artifact
↓
FastAPI Application
↓
Prediction Endpoint
↓
Frontend Application
↓
Real User
Or, in Python:
complete_ml_journey = [
"Notebook",
"Model Training",
"Model Evaluation",
"Model Persistence",
"FastAPI",
"REST API",
"Prediction Endpoint",
"Frontend Integration",
"Real-World User"
]
From Experiment to Product
There is a significant difference between saying:
I trained a machine learning model.
and saying:
I developed and evaluated a machine learning pipeline, persisted the trained model, built a FastAPI service around it, implemented request validation, exposed prediction endpoints, returned structured responses, tested the service, and connected the model to a user-facing application.
The first demonstrates knowledge of machine learning, while the second demonstrates an understanding of how machine learning becomes part of a software system.
That transition is important for anyone interested in:
- Machine Learning Engineering
- Data Science
- MLOps
- Backend Development
- Applied AI
- Production Machine Learning
Final Thought.
A machine learning model sitting inside a notebook is an experiment, a saved model is an artifact, while model exposed through an API becomes a service.
And a model connected to a usable interface begins to become a product.
machine_learning_evolution = {
"stage_1": "Experiment",
"stage_2": "Model",
"stage_3": "API",
"stage_4": "Application",
"stage_5": "Business Value"
}
That is the journey we will explore in this workshop.
From Notebook → To API → To Application → To Real-World Impact


Top comments (0)