Spaces:
Build error
Build error
Upload 12 files
Browse files- .gitignore +10 -0
- Dockerfile +19 -0
- LICENSE +21 -0
- README.md +0 -0
- app.py +51 -0
- config/city_tier.py +9 -0
- frontend.py +52 -0
- model/model.pkl +3 -0
- model/predict.py +32 -0
- requirements.txt +6 -0
- schema/prediction_response.py +19 -0
- schema/user_input.py +58 -0
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python-generated files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[oc]
|
| 4 |
+
build/
|
| 5 |
+
dist/
|
| 6 |
+
wheels/
|
| 7 |
+
*.egg-info
|
| 8 |
+
|
| 9 |
+
# Virtual environments
|
| 10 |
+
/.venv/
|
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13-slim
|
| 2 |
+
|
| 3 |
+
# Set the working directory in the container
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Copy the requirements file into the container
|
| 7 |
+
COPY requirements.txt /app/requirements.txt
|
| 8 |
+
|
| 9 |
+
# Install dependencies
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
# Copy the application code into the container
|
| 13 |
+
COPY . /app
|
| 14 |
+
|
| 15 |
+
# Expose ports for FastAPI and Streamlit
|
| 16 |
+
EXPOSE 8000 8501
|
| 17 |
+
|
| 18 |
+
# Command to run both FastAPI and Streamlit
|
| 19 |
+
CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port 8000 & streamlit run frontend.py --server.port 8501 --server.address 0.0.0.0"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Suresh Beekhani
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
File without changes
|
app.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
+
from schema.user_input import UserInput
|
| 4 |
+
from schema.prediction_response import PredictionResponse
|
| 5 |
+
from model.predict import predict_output, model, MODEL_VERSION
|
| 6 |
+
import uvicorn
|
| 7 |
+
|
| 8 |
+
app = FastAPI()
|
| 9 |
+
|
| 10 |
+
# human readable
|
| 11 |
+
@app.get('/')
|
| 12 |
+
def home():
|
| 13 |
+
return {'message':'Insurance Premium Prediction API'}
|
| 14 |
+
|
| 15 |
+
# machine readable
|
| 16 |
+
@app.get('/health')
|
| 17 |
+
def health_check():
|
| 18 |
+
return {
|
| 19 |
+
'status': 'OK',
|
| 20 |
+
'version': MODEL_VERSION,
|
| 21 |
+
'model_loaded': model is not None
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
@app.post('/predict', response_model=PredictionResponse)
|
| 25 |
+
def predict_premium(data: UserInput):
|
| 26 |
+
|
| 27 |
+
user_input = {
|
| 28 |
+
'bmi': data.bmi,
|
| 29 |
+
'age_group': data.age_group,
|
| 30 |
+
'lifestyle_risk': data.lifestyle_risk,
|
| 31 |
+
'city_tier': data.city_tier,
|
| 32 |
+
'income_lpa': data.income_lpa,
|
| 33 |
+
'occupation': data.occupation
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
|
| 38 |
+
prediction = predict_output(user_input)
|
| 39 |
+
|
| 40 |
+
return JSONResponse(status_code=200, content={'response': prediction})
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
|
| 44 |
+
return JSONResponse(status_code=500, content=str(e))
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
"""
|
| 48 |
+
Run the FastAPI app using uvicorn.
|
| 49 |
+
"""
|
| 50 |
+
uvicorn.run(app, host="127.0.0.1", port=8000, reload=True)
|
| 51 |
+
# To run the app, use the command: uvicorn app:app --reload
|
config/city_tier.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tier_1_cities = ["Mumbai", "Delhi", "Bangalore", "Chennai", "Kolkata", "Hyderabad", "Pune"]
|
| 2 |
+
tier_2_cities = [
|
| 3 |
+
"Jaipur", "Chandigarh", "Indore", "Lucknow", "Patna", "Ranchi", "Visakhapatnam", "Coimbatore",
|
| 4 |
+
"Bhopal", "Nagpur", "Vadodara", "Surat", "Rajkot", "Jodhpur", "Raipur", "Amritsar", "Varanasi",
|
| 5 |
+
"Agra", "Dehradun", "Mysore", "Jabalpur", "Guwahati", "Thiruvananthapuram", "Ludhiana", "Nashik",
|
| 6 |
+
"Allahabad", "Udaipur", "Aurangabad", "Hubli", "Belgaum", "Salem", "Vijayawada", "Tiruchirappalli",
|
| 7 |
+
"Bhavnagar", "Gwalior", "Dhanbad", "Bareilly", "Aligarh", "Gaya", "Kozhikode", "Warangal",
|
| 8 |
+
"Kolhapur", "Bilaspur", "Jalandhar", "Noida", "Guntur", "Asansol", "Siliguri"
|
| 9 |
+
]
|
frontend.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import requests
|
| 3 |
+
|
| 4 |
+
API_URL = "http://127.0.0.1:8000"
|
| 5 |
+
|
| 6 |
+
st.title("Insurance Premium Category Predictor")
|
| 7 |
+
st.markdown("Enter your details below:")
|
| 8 |
+
|
| 9 |
+
# Input fields
|
| 10 |
+
age = st.number_input("Age", min_value=1, max_value=119, value=30)
|
| 11 |
+
weight = st.number_input("Weight (kg)", min_value=1.0, value=65.0)
|
| 12 |
+
height = st.number_input("Height (m)", min_value=0.5, max_value=2.5, value=1.7)
|
| 13 |
+
income_lpa = st.number_input("Annual Income (LPA)", min_value=0.1, value=10.0)
|
| 14 |
+
smoker = st.selectbox("Are you a smoker?", options=[True, False])
|
| 15 |
+
city = st.text_input("City", value="Mumbai")
|
| 16 |
+
occupation = st.selectbox(
|
| 17 |
+
"Occupation",
|
| 18 |
+
['retired', 'freelancer', 'student', 'government_job', 'business_owner', 'unemployed', 'private_job']
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
if st.button("Predict Premium Category"):
|
| 22 |
+
input_data = {
|
| 23 |
+
"age": age,
|
| 24 |
+
"weight": weight,
|
| 25 |
+
"height": height,
|
| 26 |
+
"income_lpa": income_lpa,
|
| 27 |
+
"smoker": smoker,
|
| 28 |
+
"city": city,
|
| 29 |
+
"occupation": occupation
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
response = requests.post(API_URL + "/predict", json=input_data, timeout=10)
|
| 34 |
+
response.raise_for_status() # Raise HTTPError for bad responses (4xx and 5xx)
|
| 35 |
+
|
| 36 |
+
result = response.json()
|
| 37 |
+
if "response" in result:
|
| 38 |
+
prediction = result["response"]
|
| 39 |
+
st.success(f"Predicted Insurance Premium Category: **{prediction['predicted_category']}**")
|
| 40 |
+
st.markdown(f"**Confidence:** {prediction['confidence']:.2f}")
|
| 41 |
+
st.markdown("### Class Probabilities:")
|
| 42 |
+
st.json(prediction["class_probabilities"])
|
| 43 |
+
else:
|
| 44 |
+
st.error("Unexpected response format from the API.")
|
| 45 |
+
st.json(result)
|
| 46 |
+
|
| 47 |
+
except requests.exceptions.ConnectionError:
|
| 48 |
+
st.error("❌ Could not connect to the FastAPI server. Please ensure it is running.")
|
| 49 |
+
except requests.exceptions.Timeout:
|
| 50 |
+
st.error("⏳ The request timed out. Please try again later.")
|
| 51 |
+
except requests.exceptions.RequestException as e:
|
| 52 |
+
st.error(f"⚠️ An error occurred: {e}")
|
model/model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:99679b393250929802df797963a88de637f6d6f8304277d813f730bfdf424ab9
|
| 3 |
+
size 473714
|
model/predict.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
# import the ml model
|
| 5 |
+
with open('model/model.pkl', 'rb') as f:
|
| 6 |
+
model = pickle.load(f)
|
| 7 |
+
|
| 8 |
+
# MLFlow
|
| 9 |
+
MODEL_VERSION = '1.0.0'
|
| 10 |
+
|
| 11 |
+
# Get class labels from model (important for matching probabilities to class names)
|
| 12 |
+
class_labels = model.classes_.tolist()
|
| 13 |
+
|
| 14 |
+
def predict_output(user_input: dict):
|
| 15 |
+
|
| 16 |
+
df = pd.DataFrame([user_input])
|
| 17 |
+
|
| 18 |
+
# Predict the class
|
| 19 |
+
predicted_class = model.predict(df)[0]
|
| 20 |
+
|
| 21 |
+
# Get probabilities for all classes
|
| 22 |
+
probabilities = model.predict_proba(df)[0]
|
| 23 |
+
confidence = max(probabilities)
|
| 24 |
+
|
| 25 |
+
# Create mapping: {class_name: probability}
|
| 26 |
+
class_probs = dict(zip(class_labels, map(lambda p: round(p, 4), probabilities)))
|
| 27 |
+
|
| 28 |
+
return {
|
| 29 |
+
"predicted_category": predicted_class,
|
| 30 |
+
"confidence": round(confidence, 4),
|
| 31 |
+
"class_probabilities": class_probs
|
| 32 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
requests
|
| 5 |
+
streamlit
|
| 6 |
+
pandas
|
schema/prediction_response.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Dict
|
| 3 |
+
|
| 4 |
+
class PredictionResponse(BaseModel):
|
| 5 |
+
predicted_category: str = Field(
|
| 6 |
+
...,
|
| 7 |
+
description="The predicted insurance premium category",
|
| 8 |
+
example="High"
|
| 9 |
+
)
|
| 10 |
+
confidence: float = Field(
|
| 11 |
+
...,
|
| 12 |
+
description="Model's confidence score for the predicted class (range: 0 to 1)",
|
| 13 |
+
example=0.8432
|
| 14 |
+
)
|
| 15 |
+
class_probabilities: Dict[str, float] = Field(
|
| 16 |
+
...,
|
| 17 |
+
description="Probability distribution across all possible classes",
|
| 18 |
+
example={"Low": 0.01, "Medium": 0.15, "High": 0.84}
|
| 19 |
+
)
|
schema/user_input.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field, computed_field, field_validator
|
| 2 |
+
from typing import Literal, Annotated
|
| 3 |
+
from config.city_tier import tier_1_cities, tier_2_cities
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# pydantic model to validate incoming data
|
| 7 |
+
class UserInput(BaseModel):
|
| 8 |
+
|
| 9 |
+
age: Annotated[int, Field(..., gt=0, lt=120, description='Age of the user')]
|
| 10 |
+
weight: Annotated[float, Field(..., gt=0, description='Weight of the user')]
|
| 11 |
+
height: Annotated[float, Field(..., gt=0, lt=2.5, description='Height of the user')]
|
| 12 |
+
income_lpa: Annotated[float, Field(..., gt=0, description='Annual salary of the user in lpa')]
|
| 13 |
+
smoker: Annotated[bool, Field(..., description='Is user a smoker')]
|
| 14 |
+
city: Annotated[str, Field(..., description='The city that the user belongs to')]
|
| 15 |
+
occupation: Annotated[Literal['retired', 'freelancer', 'student', 'government_job',
|
| 16 |
+
'business_owner', 'unemployed', 'private_job'], Field(..., description='Occupation of the user')]
|
| 17 |
+
|
| 18 |
+
@field_validator('city')
|
| 19 |
+
@classmethod
|
| 20 |
+
def normalize_city(cls, v: str) -> str:
|
| 21 |
+
v = v.strip().title()
|
| 22 |
+
return v
|
| 23 |
+
|
| 24 |
+
@computed_field
|
| 25 |
+
@property
|
| 26 |
+
def bmi(self) -> float:
|
| 27 |
+
return self.weight/(self.height**2)
|
| 28 |
+
|
| 29 |
+
@computed_field
|
| 30 |
+
@property
|
| 31 |
+
def lifestyle_risk(self) -> str:
|
| 32 |
+
if self.smoker and self.bmi > 30:
|
| 33 |
+
return "high"
|
| 34 |
+
elif self.smoker or self.bmi > 27:
|
| 35 |
+
return "medium"
|
| 36 |
+
else:
|
| 37 |
+
return "low"
|
| 38 |
+
|
| 39 |
+
@computed_field
|
| 40 |
+
@property
|
| 41 |
+
def age_group(self) -> str:
|
| 42 |
+
if self.age < 25:
|
| 43 |
+
return "young"
|
| 44 |
+
elif self.age < 45:
|
| 45 |
+
return "adult"
|
| 46 |
+
elif self.age < 60:
|
| 47 |
+
return "middle_aged"
|
| 48 |
+
return "senior"
|
| 49 |
+
|
| 50 |
+
@computed_field
|
| 51 |
+
@property
|
| 52 |
+
def city_tier(self) -> int:
|
| 53 |
+
if self.city in tier_1_cities:
|
| 54 |
+
return 1
|
| 55 |
+
elif self.city in tier_2_cities:
|
| 56 |
+
return 2
|
| 57 |
+
else:
|
| 58 |
+
return 3
|