Spaces:
Sleeping
Sleeping
File size: 5,783 Bytes
d5a00ba | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """
FastAPI backend for flight price prediction — Hugging Face Spaces version.
Downloads model artifacts from HF Hub at startup and serves predictions.
Endpoints:
GET / -> Health check
GET /metadata -> Dropdown options for the frontend
POST /predict -> Predict flight price
"""
import json
import os
import joblib
import numpy as np
import pandas as pd
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from huggingface_hub import hf_hub_download
from pydantic import BaseModel, Field
# ---------- Configuration ----------
MODEL_REPO = os.getenv("HF_MODEL_REPO", "Sajjad-Ali-Shah/skypredict-flight-price")
CACHE_DIR = "/tmp/hf_cache"
# ---------- Download artifacts from HF Hub ----------
print(f"Downloading model from {MODEL_REPO}...")
model_path = hf_hub_download(repo_id=MODEL_REPO, filename="model.joblib", cache_dir=CACHE_DIR)
scaler_path = hf_hub_download(repo_id=MODEL_REPO, filename="scaler.joblib", cache_dir=CACHE_DIR)
metadata_path = hf_hub_download(repo_id=MODEL_REPO, filename="metadata.json", cache_dir=CACHE_DIR)
print("Model artifacts downloaded!")
# ---------- Load artifacts ----------
model = joblib.load(model_path)
scaler = joblib.load(scaler_path)
with open(metadata_path) as f:
metadata = json.load(f)
FEATURE_NAMES = metadata["feature_names"]
SCALE_COLS = metadata["scale_cols"]
STOPS_MAP = metadata["stops_map"]
# ---------- App ----------
app = FastAPI(
title="SkyPredict — Flight Price API",
description="Predict airline flight ticket prices using a Random Forest model (R² = 0.98)",
version="1.0.0",
)
# Allow requests from Vercel dashboard + local dev
ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
# Add any Vercel production URL from env
vercel_url = os.getenv("FRONTEND_URL", "")
if vercel_url:
ALLOWED_ORIGINS.append(vercel_url)
# Also allow all *.vercel.app preview deployments
ALLOWED_ORIGINS.append("https://*.vercel.app")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Permissive for HF Spaces; tighten if needed
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---------- Schemas ----------
class PredictionRequest(BaseModel):
airline: str = Field(..., example="Vistara")
source_city: str = Field(..., example="Delhi")
destination_city: str = Field(..., example="Mumbai")
departure_time: str = Field(..., example="Morning")
arrival_time: str = Field(..., example="Night")
stops: str = Field(..., example="one")
class_type: str = Field(..., example="Business")
duration: float = Field(..., ge=0.5, le=50, example=5.5)
days_left: int = Field(..., ge=1, le=49, example=15)
class PredictionResponse(BaseModel):
predicted_price: int
currency: str = "INR"
model: str
r2_score: float
# ---------- Routes ----------
@app.get("/")
def health_check():
return {
"status": "healthy",
"model": metadata["model_name"],
"r2_score": metadata["r2_score"],
}
@app.get("/metadata")
def get_metadata():
"""Return dropdown options and value ranges for the frontend."""
return {
"categories": metadata["categories"],
"duration_range": metadata["duration_range"],
"days_left_range": metadata["days_left_range"],
}
@app.post("/predict", response_model=PredictionResponse)
def predict(req: PredictionRequest):
"""Predict the price of a flight ticket."""
try:
# Build a single-row DataFrame with the same columns as training
stops_numeric = STOPS_MAP.get(req.stops, 1)
is_direct = 1 if req.stops == "zero" else 0
class_val = 1 if req.class_type == "Business" else 0
# Start with numeric features
row = {
"duration": req.duration,
"days_left": req.days_left,
"is_direct": is_direct,
"stops_numeric": stops_numeric,
"class": class_val,
}
# One-hot encode categoricals (drop_first=True matches training)
onehot_config = {
"airline": metadata["categories"]["airline"],
"source_city": metadata["categories"]["source_city"],
"destination_city": metadata["categories"]["destination_city"],
"departure_time": metadata["categories"]["departure_time"],
"arrival_time": metadata["categories"]["arrival_time"],
}
for feature, all_values in onehot_config.items():
sorted_vals = sorted(all_values)
for val in sorted_vals[1:]: # skip first (drop_first=True)
col_name = f"{feature}_{val}"
actual_val = getattr(req, feature)
row[col_name] = 1 if actual_val == val else 0
# Create DataFrame and align columns with training order
df_input = pd.DataFrame([row])
# Ensure all expected columns are present
for col in FEATURE_NAMES:
if col not in df_input.columns:
df_input[col] = 0
df_input = df_input[FEATURE_NAMES]
# Scale numeric columns
df_input[SCALE_COLS] = scaler.transform(df_input[SCALE_COLS])
# Predict
prediction = model.predict(df_input)[0]
predicted_price = max(int(round(prediction)), 0)
return PredictionResponse(
predicted_price=predicted_price,
currency="INR",
model=metadata["model_name"],
r2_score=metadata["r2_score"],
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
|