| |
| from fastapi import FastAPI, HTTPException |
| from pydantic import BaseModel |
| from typing import List, Literal |
| import joblib |
| import numpy as np |
|
|
| app = FastAPI(title="Transaction Categorizer") |
|
|
| |
| shop_model = joblib.load("shop_classifier.pkl") |
| category_model = joblib.load("category_classifier.pkl") |
|
|
| def predict_or_na(pipeline, text: str, threshold: float = 0.3) -> str: |
| """ |
| Returns the top class if its probability ≥ threshold, else "N/A" |
| """ |
| probs = pipeline.predict_proba([text])[0] |
| top_idx = int(np.argmax(probs)) |
| return str(pipeline.classes_[top_idx]) if probs[top_idx] >= threshold else "N/A" |
|
|
| class TransactionIn(BaseModel): |
| id: str |
| description: str |
|
|
| class TransactionOut(BaseModel): |
| id: str |
| description: str |
| shop: str |
| category: str |
|
|
| @app.post("/predict", response_model=List[TransactionOut]) |
| def predict_transactions( |
| items: List[TransactionIn], |
| threshold: float = 0.3 |
| ): |
| """ |
| Predict shop and category for each transaction. |
| - **items**: list of `{ id, description }` |
| - **threshold**: optional override for probability cutoff |
| """ |
| results = [] |
| for item in items: |
| desc_norm = item.description.lower().strip() |
| shop_pred = predict_or_na(shop_model, desc_norm, threshold) |
| category_pred = predict_or_na(category_model, desc_norm, threshold) |
| results.append( |
| TransactionOut( |
| id=item.id, |
| description=item.description, |
| shop=shop_pred, |
| category=category_pred |
| ) |
| ) |
| return results |
|
|
| |
| @app.get("/health") |
| def health(): |
| return {"status": "ok"} |