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))