So far, we’ve built a complete FastAPI application and implemented the core CRUD operations.
But FastAPI becomes even more powerful when we use it to serve a machine learning model.
A trained model by itself is not enough to make predictions available to other applications. We need a way for clients to send input data, have the model process that data, and receive a prediction in response.
This is where FastAPI and machine learning come together.
In this part, we’ll build an Insurance Premium Prediction API.
We’ll take a trained Random Forest Classifier, save it as a .pkl file, load it into FastAPI, prepare the required features from user input, and expose the model through a POST /predict endpoint.
Finally, we’ll connect the API to a Streamlit frontend so users can enter their information and receive an insurance premium category prediction.
Overview
- Building and Exporting the ML Model
- Feature Engineering for Prediction
- Understanding the Project Flow
- Loading the ML Model in FastAPI
- Creating the Pydantic Input Model
- Using Computed Fields for Feature Engineering
- Building the Prediction Endpoint
- Preparing Input Data for the Model
- Generating Real-Time Predictions
- Testing the API
- Connecting FastAPI with Streamlit
- Complete Code Example
- Output
- Code Execution Explanation
- Key Takeaways
- Conclusion
- What’s Next?
Building and Exporting the ML Model
Before connecting a machine learning model to FastAPI, we first need a trained model that can make predictions.
For this project, we are building an Insurance Premium Prediction model using a Random Forest Classifier. The training data comes from insurance.csv, and the target variable is insurance_premium_category.
The model uses the following features:
- BMI
- Age Group
- Lifestyle Risk
- City Tier
- Income (LPA)
- Occupation
These features are prepared from the original user data before training the model.
Creating the Model Pipeline
The project uses a ColumnTransformer to handle categorical and numerical features separately.
Categorical features are:
age_grouplifestyle_riskoccupationcity_tier
Numerical features are:
bmiincome_lpa
The categorical features are converted using OneHotEncoder, while the numerical features are passed through unchanged.
categorical_features = ["age_group", "lifestyle_risk", "occupation", "city_tier"]
numeric_features = ["bmi", "income_lpa"]
preprocessor = ColumnTransformer(
transformers=[
("cat", OneHotEncoder(), categorical_features),
("num", "passthrough", numeric_features)
]
)Next, we combine the preprocessing step with a Random Forest Classifier using a scikit-learn pipeline:
pipeline = Pipeline(steps=[
("preprocessor", preprocessor),
("classifier", RandomForestClassifier(random_state=42))
])This allows preprocessing and prediction to be handled as a single pipeline.
Training the Model
We split the dataset into training and testing sets and then train the pipeline:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=1
)
pipeline.fit(X_train, y_train)The notebook uses 80% of the data for training and 20% for testing.
Evaluating the Model
After training, we make predictions on the test data and calculate the accuracy:
y_pred = pipeline.predict(X_test)
accuracy_score(y_test, y_pred)Output
0.9The trained model achieved an accuracy score of 0.90 on the test set in the notebook.
Exporting the Trained Model
Once the model is trained, we need to save it so that the FastAPI application can load it later without retraining the model.
The project uses Python’s pickle module to save the complete trained pipeline:
import pickle
pickle_model_path = "model.pkl"
with open(pickle_model_path, "wb") as f:
pickle.dump(pipeline, f)This creates the model.pkl file containing the trained pipeline.
Code Execution Explanation
The overall process is:
Dataset → Feature Engineering → Preprocessing → Random Forest → Training → Evaluation → model.pkl
The important part is that we save the entire pipeline, not just the Random Forest classifier. This allows the same preprocessing used during training to remain part of the saved model that FastAPI will later load for predictions.
Feature Engineering for Prediction
A trained machine learning model expects input in a specific format. The data submitted by a user through our API, however, is not exactly the same as the features used during model training.
For example, the API receives:
ageweightheightincome_lpasmokercityoccupation
But the trained model expects:
bmiage_grouplifestyle_riskcity_tierincome_lpaoccupation
Therefore, before making a prediction, we need to transform the user’s raw input into the features expected by the model.
Calculating BMI
BMI is derived from the user’s weight and height:
@computed_field
@property
def bmi(self) -> float:
return self.weight / (self.height**2)The API does not ask the user to provide BMI directly. Instead, it calculates BMI from weight and height.
Creating Lifestyle Risk
The model also uses a lifestyle_risk feature.
It is derived from two pieces of information:
- Whether the user is a smoker
- The user’s BMI
@computed_field
@property
def lifestyle_risk(self) -> str:
if self.smoker and self.bmi > 30:
return "high"
elif self.smoker or self.bmi > 27:
return "medium"
else:
return "low"This converts the raw smoker and calculated bmi values into one of three categories: high, medium, or low.
Creating Age Groups
Instead of passing the numerical age directly as an age category, the application derives an age_group:
@computed_field
@property
def age_group(self) -> str:
if self.age < 25:
return "young"
elif self.age < 45:
return "adult"
elif self.age < 60:
return "middle_aged"
return "senior"This transforms the user’s age into one of four categories:
youngadultmiddle_agedsenior
Converting City into City Tier
The model does not use the city name directly. Instead, the application converts the city into a numerical city_tier.
The project maintains separate lists for Tier 1 and Tier 2 cities:
tier_1_cities = [
"Mumbai", "Delhi", "Bangalore", "Chennai",
"Kolkata", "Hyderabad", "Pune"
]If the city belongs to Tier 1, it returns 1. If it belongs to Tier 2, it returns 2. Otherwise, it returns 3.
Creating the Model Input
Once these derived features are available, the API constructs a DataFrame containing exactly the features required by the trained model:
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])This is the important transition:
Raw User Input → Derived Features → Model Input
The resulting DataFrame is then ready to be passed to the trained model.
Code Execution Explanation
The feature-engineering process takes information that is easy for a user to provide and converts it into the structured features expected by the machine learning model.
For example:
Age + Weight + Height
↓
BMI
↓
Age → Age Group
Smoker + BMI → Lifestyle Risk
City → City Tier
↓
Model FeaturesThis separation is important because the API input format and model feature format do not have to be identical. The API acts as the layer that transforms user-friendly input into model-ready data.
Understanding the Project Flow
Before looking at the FastAPI implementation, it is useful to understand how the different parts of the project work together.
The project is divided into three main steps:
- Model Building — Train the insurance premium prediction model and export it as
model.pkl. - FastAPI — Load the saved model, accept user input, generate the features required by the model, and return a prediction.
- Streamlit Frontend — Provide a user interface where users can enter their information and receive the predicted premium category.
Complete Project Flow
The overall flow can be understood as:
Model Building → model.pkl → FastAPI API → Streamlit Frontend
The model-building step produces the trained model.pkl file. The FastAPI application then loads this file and exposes a /predict endpoint for inference.
When a user interacts with the frontend, the flow becomes:
User Input → Streamlit → FastAPI /predict → Feature Engineering → ML Model → Prediction → Streamlit
The FastAPI application receives fields such as age, weight, height, income, smoking status, city, and occupation. It then derives the features required by the model, creates a Pandas DataFrame, and passes that data to model.predict().
What Happens Inside FastAPI?
The FastAPI layer acts as the bridge between the frontend and the machine learning model.
Its responsibility is to:
- Load the trained
model.pkl. - Validate incoming user data using a Pydantic model.
- Calculate derived features such as BMI, lifestyle risk, age group, and city tier.
- Prepare those features as a Pandas DataFrame.
- Send the DataFrame to the trained model.
- Return the predicted premium category as a JSON response.
This separation is important because the machine learning model does not directly interact with the user interface. FastAPI sits between them and handles the API and prediction logic.
The Three Layers
You can think of the project as three connected layers:
1. Machine Learning Layer
The trained model is exported as:
model.pkl
2. API Layer
FastAPI loads the model and exposes:
POST /predict
The endpoint receives validated input, prepares the model features, and generates the prediction.
3. Frontend Layer
Streamlit provides the interface through which users enter their information and consume the prediction returned by the API.
This gives us a simple architecture:
Frontend → API → ML Model
And for the response:
ML Model → API → Frontend
This section should stay at the architecture/flow level. We won’t repeat the details of model training or feature engineering here; those are covered in their respective sections.
Why This Flow Matters
A machine learning model by itself is usually not enough for a real application. The model needs a way to receive input from other applications and return predictions.
FastAPI provides that connection.
In this project: Streamlit handles the user interface, FastAPI handles communication and inference, and the trained model handles prediction.
Once this flow is clear, we can move into the FastAPI implementation and see exactly how the prediction endpoint connects these components.
Loading the ML Model in FastAPI
Once the machine learning model has been trained and exported, the next step is to make it available to the FastAPI application.
In our project, the trained model is stored as a model.pkl file. FastAPI loads this file when the application starts, so the model is ready to handle prediction requests.
Loading the Saved Model
The project uses Python’s pickle module to load the previously exported model:
import pickle
with open('model.pkl', 'rb') as f:
model = pickle.load(f)Here:
pickleis used to deserialize the saved Python model.'model.pkl'is the file containing the trained model.'rb'opens the file in read-binary mode.pickle.load(f)reconstructs the saved model and stores it in themodelvariable.
The FastAPI application is then initialized:
app = FastAPI()This gives us two important objects:
model→ Trained ML model loaded frommodel.pklapp→ FastAPI application
The important point here is that the model is loaded once when the application initializes, rather than being loaded inside every prediction request.
model.pkl → Load Model → FastAPI Application
At this stage, FastAPI has access to the trained model, but no prediction has been made yet.
Creating the Pydantic Input Model
Before the FastAPI endpoint can make a prediction, it needs a structured way to receive and validate user input.
For this project, we use a Pydantic model called UserInput. It defines the fields that the API expects from the client and adds validation rules to those fields.
Defining the Input Schema
The UserInput model contains the information required from the user:
class UserInput(BaseModel):
age: Annotated[int, Field(..., gt=0, lt=120, description='Age of the user')]
weight: Annotated[float, Field(..., gt=0, description='Weight of the user')]
height: Annotated[float, Field(..., gt=0, lt=250, description='Height of the user')]
income_lpa: Annotated[float, Field(..., gt=0, description='Annual salary of the user in lpa')]
smoker: Annotated[bool, Field(..., description='Is user a smoker')]
city: Annotated[str, Field(..., description='The city that the user belongs to')]
occupation: Annotated[
Literal[
'retired',
'freelancer',
'student',
'government_job',
'business_owner',
'unemployed',
'private_job'
],
Field(..., description='Occupation of the user')
]This model expects seven input fields:
ageweightheightincome_lpasmokercityoccupation
The Field() definitions also provide validation constraints and descriptions. For example, age must be greater than 0 and less than 120, while height must be greater than 0 and less than 250.
Restricting Occupation Values
The occupation field uses Literal to restrict the accepted values:
Literal[
'retired',
'freelancer',
'student',
'government_job',
'business_owner',
'unemployed',
'private_job'
]This means the API does not accept arbitrary occupation strings. The incoming value must match one of the defined categories.
Why Use Pydantic Here?
The Pydantic model creates a clear boundary between external user input and the internal prediction logic.
Instead of allowing the prediction endpoint to receive unstructured data, FastAPI can validate the request against UserInput first.
The flow becomes:
Client Input → UserInput Validation → Validated Data → Feature Engineering → Prediction
The computed fields inside this model are responsible for deriving the additional features required by the trained model. We’ll examine that process separately in the Feature Engineering for Prediction section rather than repeating it here.
This keeps the responsibilities clear:
UserInput→ defines and validates incoming data.- Computed fields → derive model features.
- Prediction endpoint → sends those features to the loaded ML model.
Using Computed Fields for Feature Engineering
The API receives raw user information, but the trained machine learning model expects a different set of features.
For example, the user provides:
ageweightheightsmokercityincome_lpaoccupation
The model, however, works with features such as:
bmiage_grouplifestyle_riskcity_tierincome_lpaoccupation
Instead of asking the user to provide these derived values manually, the UserInput Pydantic model calculates them automatically using computed fields.
Calculating BMI
BMI is calculated from the user’s weight and height:
@computed_field
@property
def bmi(self) -> float:
return self.weight / (self.height ** 2)Whenever the model accesses data.bmi, the value is calculated from the validated weight and height.
Calculating Lifestyle Risk
The application also derives a lifestyle_risk feature using the user's smoking status and BMI:
@computed_field
@property
def lifestyle_risk(self) -> str:
if self.smoker and self.bmi > 30:
return "high"
elif self.smoker or self.bmi > 27:
return "medium"
else:
return "low"The logic produces three possible categories:
- High — the user is a smoker and has a BMI above 30.
- Medium — the user is a smoker or has a BMI above 27.
- Low — neither condition is satisfied.
Creating an Age Group
The raw age value is converted into an age_group:
@computed_field
@property
def age_group(self) -> str:
if self.age < 25:
return "young"
elif self.age < 45:
return "adult"
elif self.age < 60:
return "middle_aged"
return "senior"This converts a numerical age into one of four categories:
youngadultmiddle_agedsenior
Determining the City Tier
The application also derives a city_tier from the user's city:
@computed_field
@property
def city_tier(self) -> int:
if self.city in tier_1_cities:
return 1
elif self.city in tier_2_cities:
return 2
else:
return 3The application maintains separate lists for Tier 1 and Tier 2 cities. Cities that do not belong to either list are assigned Tier 3.
From Raw Input to Model Features
These computed fields allow the API to transform user-friendly input into the feature representation expected by the trained model.
The transformation can be summarized as:
Raw User Input → Computed Features → Model-Ready Features
For example:
Age + Weight + Height
↓
BMI + Age Group
Smoker + BMI
↓
Lifestyle Risk
City
↓
City TierThe resulting features are later assembled into a Pandas DataFrame containing:
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])This DataFrame represents the final input that will be passed to the trained model.
The important idea is that feature engineering happens inside the API, so the client only needs to provide the raw information. FastAPI and Pydantic take care of preparing the derived features required for prediction.
Client provides raw data → Pydantic validates it → Computed fields derive features → API prepares model input
Building the Prediction Endpoint
With the ML model loaded and the UserInput model handling incoming data, we can now connect everything through a FastAPI endpoint.
The purpose of this endpoint is simple:
Receive user input → Prepare model features → Generate prediction → Return the result
Our project uses a POST endpoint at /predict for this purpose.
Defining the Endpoint
The prediction endpoint is defined as:
@app.post('/predict')
def predict_premium(data: UserInput):
...Here:
@app.post('/predict')tells FastAPI to handlePOSTrequests sent to/predict.data: UserInputtells FastAPI to validate the request body using theUserInputPydantic model.datacontains the validated user information.
This means the endpoint does not need to manually validate every incoming field. FastAPI and Pydantic handle that before the prediction logic runs.
Preparing the Model Input
After validation, the endpoint creates a Pandas DataFrame containing the features required by the trained model:
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])At this point, the raw request data has already been transformed into the model-ready feature structure.
Notice that the endpoint does not recalculate BMI, age group, lifestyle risk, or city tier itself. Those values are obtained from the computed fields defined earlier.
This keeps the endpoint focused on connecting the prepared data to the model.
Generating the Prediction
The prepared DataFrame is passed to the loaded model:
prediction = model.predict(input_df)[0]The model generates a prediction, and [0] extracts the prediction for the single input row.
Finally, the API returns the prediction as a JSON response:
return JSONResponse(
status_code=200,
content={'predicted_category': prediction}
)The client therefore receives a response containing the predicted premium category.
Complete Endpoint
Putting the endpoint logic together:
@app.post('/predict')
def predict_premium(data: UserInput):
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])
prediction = model.predict(input_df)[0]
return JSONResponse(
status_code=200,
content={'predicted_category': prediction}
)The endpoint now connects the three pieces we have built so far:
User Input → Pydantic Validation → Prepared Features → ML Model → JSON Response
The FastAPI layer is therefore acting as the interface between the application and the trained machine learning model.
Preparing Input Data for the Model
After validating the incoming request and generating the required computed features, the next step is to prepare the data in the exact structure expected by the trained model.
The prediction endpoint converts the validated UserInput object into a Pandas DataFrame containing the six features used for prediction.
Creating the Model Input
The input DataFrame is created as follows:
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])Here, the values come from the validated data object:
data.bmi→ calculated BMIdata.age_group→ derived age categorydata.lifestyle_risk→ derived lifestyle riskdata.city_tier→ derived city classificationdata.income_lpa→ user's annual incomedata.occupation→ user's occupation
The resulting DataFrame represents one user’s prediction input.
Conceptually:
Raw User Input
↓
Pydantic Validation
↓
Computed Features
↓
Pandas DataFrame
↓
ML ModelWhy Use a DataFrame?
The trained model is used with tabular data, so the API prepares the incoming values in a Pandas DataFrame before passing them to the model.
This also keeps the prediction input organized by feature name, rather than passing individual values separately.
The DataFrame contains:
bmiage_grouplifestyle_riskcity_tierincome_lpaoccupation
Once this input_df is prepared, it can be directly passed to the loaded model:
prediction = model.predict(input_df)[0]So this section’s responsibility is specifically transforming the validated API data into the tabular format required for inference. The actual prediction step belongs to the next stage.
Validated Input → Feature Values → DataFrame → Model-Ready Input
Generating Real-Time Predictions
Once the input data has been prepared in the format expected by the model, the final step is to generate a prediction.
The trained model is already loaded when the FastAPI application starts. The /predict endpoint can therefore use that model directly whenever a client sends a request.
Making the Prediction
The prediction is generated with:
prediction = model.predict(input_df)[0]Here:
input_dfcontains the prepared features for the user.model.predict()passes those features to the trained machine learning model.[0]extracts the prediction from the returned result because the DataFrame contains a single input row.
The important point is that no model training happens during an API request. The API uses the already-trained model to perform inference.
Returning the Prediction
After generating the prediction, the API returns it to the client as a JSON response:
return JSONResponse(
status_code=200,
content={'predicted_category': prediction}
)A successful response has the following structure:
{
"predicted_category": "High"
}The actual category is produced by the trained model.
Complete Prediction Flow
At this point, the complete inference process is:
POST Request → Pydantic Validation → Computed Features → DataFrame → model.predict() → JSON Response
This is what makes the model available for real-time inference through an API.
The FastAPI application has now completed its core responsibility: receiving user data, preparing it for the model, generating a prediction, and returning that prediction to the client.
Testing the API
After building the prediction endpoint, the next step is to test whether the API correctly accepts user input and returns a prediction.
Sending a Prediction Request
Our FastAPI application exposes the following endpoint:
POST /predictThe request body should contain the fields defined by the UserInput model:
{
"age": 30,
"weight": 70,
"height": 1.75,
"income_lpa": 10,
"smoker": false,
"city": "Jaipur",
"occupation": "private_job"
}FastAPI validates this request against the UserInput model before the prediction logic executes.
What Happens During the Request?
Once the request reaches /predict, the API:
- Receives the user’s input.
- Validates the data using Pydantic.
- Uses the computed fields to generate the required features.
- Creates the model input DataFrame.
- Passes the DataFrame to the trained model.
- Returns the predicted category.
The complete request flow is:
POST /predict → Validate Input → Generate Features → Prepare DataFrame → model.predict() → Return Prediction
Example Response
The endpoint returns the prediction in JSON format:
{
"predicted_category": "High"
}The response is returned with an HTTP 200 status code.
The exact category depends on the input values and the trained model’s prediction.
Testing Through FastAPI Documentation
FastAPI automatically provides interactive API documentation, so the /predict endpoint can be tested directly through the Swagger UI.
You can:
- Open the
/docspage. - Select
POST /predict. - Click Try it out.
- Enter the JSON request body.
- Execute the request.
- Inspect the returned prediction.
This provides a convenient way to verify the complete API flow before connecting a frontend.
API Test: Client Request → FastAPI → ML Model → Prediction Response
Once the endpoint works correctly, the same API can be consumed by the Streamlit frontend to provide a user-facing prediction interface.
Connecting FastAPI with Streamlit
The FastAPI prediction API can now be connected to a user-facing application using Streamlit.
In this project, Streamlit acts as the frontend, while FastAPI remains responsible for receiving the input, preparing the features, running the ML model, and returning the prediction.
The overall architecture is:
User → Streamlit → FastAPI → ML Model → FastAPI → Streamlit → User
Configuring the API URL
The Streamlit application stores the FastAPI endpoint URL:
API_URL = "http://localhost:8000/predict"This is the endpoint that Streamlit will call when the user requests a prediction.
Collecting User Input
The frontend provides input controls for the same fields expected by the FastAPI UserInput model:
age = st.number_input("Age", min_value=1, max_value=119, value=30)
weight = st.number_input("Weight (kg)", min_value=1.0, value=65.0)
height = st.number_input("Height (m)", min_value=0.5, max_value=2.5, value=1.7)
income_lpa = st.number_input("Annual Income (LPA)", min_value=0.1, value=10.0)
smoker = st.selectbox("Are you a smoker?", options=[True, False])
city = st.text_input("City", value="Mumbai")
occupation = st.selectbox(
"Occupation",
['retired', 'freelancer', 'student', 'government_job',
'business_owner', 'unemployed', 'private_job']
)These controls allow the user to provide all the raw values required by the API.
Sending Data to FastAPI
When the user clicks the prediction button, Streamlit collects the input into a Python dictionary:
input_data = {
"age": age,
"weight": weight,
"height": height,
"income_lpa": income_lpa,
"smoker": smoker,
"city": city,
"occupation": occupation
}It then sends this data to the FastAPI endpoint using the requests library:
response = requests.post(API_URL, json=input_data)
result = response.json()The json=input_data argument sends the dictionary as the JSON request body that FastAPI expects.
Displaying the Prediction
After receiving the API response, Streamlit checks whether the request was successful and whether the response contains predicted_category:
if response.status_code == 200 and "predicted_category" in result:
prediction = result["predicted_category"]
st.success(
f"Predicted Insurance Premium Category: **{prediction}**"
)If the API returns an error, the frontend displays the status code and response:
else:
st.error(f"API Error: {response.status_code}")
st.write(result)The application also handles a connection failure:
except requests.exceptions.ConnectionError:
st.error(
"Could not connect to the FastAPI server. "
"Make sure it's running."
)Complete Frontend-to-API Flow
The complete interaction is now:
User enters details
↓
Streamlit collects the input
↓
requests.post() sends JSON to /predict
↓
FastAPI validates and prepares the input
↓
ML model generates the prediction
↓
FastAPI returns predicted_category
↓
Streamlit displays the resultThis completes the connection between the frontend, FastAPI application, and machine learning model.
The important separation is:
- Streamlit → User interface
- FastAPI → API and inference layer
- ML model → Prediction
This architecture allows the same FastAPI prediction service to be consumed by other clients as well, rather than tying the model directly to the Streamlit interface.
Complete Code Example
Now that we have covered the individual parts of the project, we can put the complete implementation together.
The project uses two main application files:
app.py→ FastAPI backend that loads the trained model and generates predictions.frontend.py→ Streamlit frontend that collects user input and communicates with the FastAPI API.
FastAPI Backend — app.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, computed_field
from typing import Literal, Annotated
import pickle
import pandas as pd
# import the ml model
with open('model.pkl', 'rb') as f:
model = pickle.load(f)
app = FastAPI()
tier_1_cities = [
"Mumbai",
"Delhi",
"Bangalore",
"Chennai",
"Kolkata",
"Hyderabad",
"Pune"
]
tier_2_cities = [
"Jaipur", "Chandigarh", "Indore", "Lucknow", "Patna",
"Ranchi", "Visakhapatnam", "Coimbatore", "Bhopal",
"Nagpur", "Vadodara", "Surat", "Rajkot", "Jodhpur",
"Raipur", "Amritsar", "Varanasi", "Agra", "Dehradun",
"Mysore", "Jabalpur", "Guwahati", "Thiruvananthapuram",
"Ludhiana", "Nashik", "Allahabad", "Udaipur",
"Aurangabad", "Hubli", "Belgaum", "Salem", "Vijayawada",
"Tiruchirappalli", "Bhavnagar", "Gwalior", "Dhanbad",
"Bareilly", "Aligarh", "Gaya", "Kozhikode", "Warangal",
"Kolhapur", "Bilaspur", "Jalandhar", "Noida", "Guntur",
"Asansol", "Siliguri"
]
# pydantic model to validate incoming data
class UserInput(BaseModel):
age: Annotated[
int,
Field(..., gt=0, lt=120, description='Age of the user')
]
weight: Annotated[
float,
Field(..., gt=0, description='Weight of the user')
]
height: Annotated[
float,
Field(..., gt=0, lt=250, description='Height of the user')
]
income_lpa: Annotated[
float,
Field(..., gt=0, description='Annual salary of the user in lpa')
]
smoker: Annotated[
bool,
Field(..., description='Is user a smoker')
]
city: Annotated[
str,
Field(..., description='The city that the user belongs to')
]
occupation: Annotated[
Literal[
'retired',
'freelancer',
'student',
'government_job',
'business_owner',
'unemployed',
'private_job'
],
Field(..., description='Occupation of the user')
]
@computed_field
@property
def bmi(self) -> float:
return self.weight / (self.height ** 2)
@computed_field
@property
def lifestyle_risk(self) -> str:
if self.smoker and self.bmi > 30:
return "high"
elif self.smoker or self.bmi > 27:
return "medium"
else:
return "low"
@computed_field
@property
def age_group(self) -> str:
if self.age < 25:
return "young"
elif self.age < 45:
return "adult"
elif self.age < 60:
return "middle_aged"
return "senior"
@computed_field
@property
def city_tier(self) -> int:
if self.city in tier_1_cities:
return 1
elif self.city in tier_2_cities:
return 2
else:
return 3
@app.post('/predict')
def predict_premium(data: UserInput):
input_df = pd.DataFrame([{
'bmi': data.bmi,
'age_group': data.age_group,
'lifestyle_risk': data.lifestyle_risk,
'city_tier': data.city_tier,
'income_lpa': data.income_lpa,
'occupation': data.occupation
}])
prediction = model.predict(input_df)[0]
return JSONResponse(
status_code=200,
content={'predicted_category': prediction}
)This is the complete FastAPI application used in the project. It loads model.pkl, validates incoming data, generates the required computed features, prepares the DataFrame, performs inference, and returns the predicted category.
Streamlit Frontend — frontend.py
import streamlit as st
import requests
API_URL = "http://localhost:8000/predict"
st.title("Insurance Premium Category Predictor")
st.markdown("Enter your details below:")
# Input fields
age = st.number_input(
"Age",
min_value=1,
max_value=119,
value=30
)
weight = st.number_input(
"Weight (kg)",
min_value=1.0,
value=65.0
)
height = st.number_input(
"Height (m)",
min_value=0.5,
max_value=2.5,
value=1.7
)
income_lpa = st.number_input(
"Annual Income (LPA)",
min_value=0.1,
value=10.0
)
smoker = st.selectbox(
"Are you a smoker?",
options=[True, False]
)
city = st.text_input(
"City",
value="Mumbai"
)
occupation = st.selectbox(
"Occupation",
[
'retired',
'freelancer',
'student',
'government_job',
'business_owner',
'unemployed',
'private_job'
]
)
if st.button("Predict Premium Category"):
input_data = {
"age": age,
"weight": weight,
"height": height,
"income_lpa": income_lpa,
"smoker": smoker,
"city": city,
"occupation": occupation
}
try:
response = requests.post(
API_URL,
json=input_data
)
result = response.json()
if (
response.status_code == 200
and "predicted_category" in result
):
prediction = result["predicted_category"]
st.success(
f"Predicted Insurance Premium Category: **{prediction}**"
)
else:
st.error(
f"API Error: {response.status_code}"
)
st.write(result)
except requests.exceptions.ConnectionError:
st.error(
"❌ Could not connect to the FastAPI server. "
"Make sure it's running."
)The frontend collects the user’s information, sends it as JSON to http://localhost:8000/predict, reads the API response, and displays the predicted insurance premium category.
How the Complete Application Fits Together
With both files running, the application works as:
Streamlit Input → POST /predict → Pydantic Validation → Computed Features → Pandas DataFrame → ML Model → Prediction → JSON Response → Streamlit Result
Output
After running both the FastAPI backend and the Streamlit frontend, the application provides a complete prediction workflow.
When the user enters their details in the Streamlit interface and clicks Predict Premium Category, the frontend sends the data to the FastAPI /predict endpoint.
The FastAPI application processes the request and returns the predicted category:
{
"predicted_category": "High"
}The Streamlit frontend then displays the result to the user:
Predicted Insurance Premium Category: High
This response is generated from the prediction returned by the trained model through the FastAPI endpoint.
Complete Output Flow
The final application flow is:
User Input → Streamlit → FastAPI /predict → Feature Preparation → ML Model → Prediction → FastAPI Response → Streamlit Output
A successful API request returns HTTP 200 along with the predicted_category field.
If the FastAPI server is unavailable, the Streamlit application displays a connection error instead.
This completes the end-to-end machine learning prediction application using FastAPI and Streamlit.
Code Execution Explanation
When the application runs, the complete prediction process is divided between the Streamlit frontend and the FastAPI backend.
First, the user enters their details in the Streamlit interface and clicks Predict Premium Category. Streamlit collects those values into input_data and sends them as a JSON POST request to the FastAPI /predict endpoint.
On the FastAPI side, the request is received through the UserInput Pydantic model. The raw values are validated, and the computed fields generate the additional features required by the model, including BMI, lifestyle risk, age group, and city tier.
The endpoint then creates a Pandas DataFrame containing the model-ready features:
bmiage_grouplifestyle_riskcity_tierincome_lpaoccupation
This DataFrame is passed to the already-loaded ML model:
prediction = model.predict(input_df)[0]The model returns a predicted premium category, which FastAPI sends back as JSON:
{
"predicted_category": "High"
}Finally, Streamlit reads the predicted_category value and displays it to the user. If the API cannot be reached, the frontend displays a connection error instead.
The complete execution flow is therefore:
User Input → Streamlit → POST /predict → Pydantic Validation → Feature Engineering → DataFrame → ML Model → Prediction → JSON Response → Streamlit Output
This completes the end-to-end flow from user input to real-time machine learning prediction.
Key Takeaways
| Concept | Summary |
|---|---|
| ML Model Serving | A trained machine learning model can be exported and served through a FastAPI application. |
| Pydantic Validation | Pydantic models validate incoming user data before it reaches the prediction logic. |
| Computed Fields | FastAPI can generate derived features such as BMI, lifestyle risk, age group, and city tier from raw input. |
| Feature Preparation | The validated and engineered features are converted into a Pandas DataFrame that matches the model’s expected input. |
| Real-Time Prediction | The saved model generates predictions through model.predict() without retraining during each request. |
| REST API | A POST /predict endpoint provides a simple interface for sending data to the ML model. |
| Frontend Integration | Streamlit can consume the FastAPI endpoint and display the prediction through a user-friendly interface. |
| End-to-End Architecture | The project demonstrates how Frontend → FastAPI → Feature Engineering → ML Model → Prediction → Frontend can work together as a complete ML application. |
Conclusion
Serving a machine learning model through FastAPI provides a practical way to turn a trained model into an accessible API service.
In this project, we built an Insurance Premium Category Predictor where FastAPI validates user input with Pydantic, generates the required features through computed fields, prepares the data for the trained model, and returns a real-time prediction through the /predict endpoint. A Streamlit frontend then consumes this API and presents the prediction to the user.
The complete architecture can be summarized as:
User Input → Streamlit → POST /predict → Pydantic Validation → Feature Engineering → DataFrame → ML Model → Prediction → JSON Response → Streamlit Output
This demonstrates an important step in moving from training a machine learning model to actually serving it as part of an application.
What’s Next?
Now that we have built an end-to-end machine learning prediction API with FastAPI, the next step is to make the API more structured, maintainable, and production-ready.
In the next part, we’ll improve the architecture of our Insurance Premium Prediction API by separating responsibilities and adding several practical API improvements.
You’ll learn:
- Project Structure: How to organize the FastAPI application into separate folders for configuration, schemas, and machine learning logic.
- Field Validation: How to normalize the city input before using it for feature engineering.
- Health Check Endpoint: How to add a
/healthendpoint to verify that the API is running and the ML model is loaded. - Model Versioning: How to expose the ML model version through the API.
- Separation of Logic: How to move Pydantic schemas, city-tier configuration, and ML prediction logic into dedicated modules.
- Error Handling: How to use
try/exceptto handle unexpected errors during prediction. - Confidence Scores: How to return the model’s confidence along with the predicted category and class probabilities.
- Response Models: How FastAPI’s
response_modelcan define, validate, document, and control the structure of API responses.
By the end of the next part, our API will move beyond simply making predictions and become a more organized, testable, and maintainable FastAPI application.
Series: FastAPI for Machine Learning — Part 8 of 12