Spaces:
Sleeping
Sleeping
madamanastasia commited on
Commit ·
aea70bb
1
Parent(s): e66b965
Fix predict matrix format + update docs
Browse files- README.md +0 -29
- docs.html +128 -85
- main.py +65 -42
- requirements.txt +1 -0
README.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Getaround Pricing API
|
| 3 |
-
emoji: 🏢
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: false
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Production API
|
| 12 |
-
|
| 13 |
-
Base URL:
|
| 14 |
-
https://anabeldg-getaround-pricing-api.hf.space
|
| 15 |
-
|
| 16 |
-
### Health check
|
| 17 |
-
GET `/health`
|
| 18 |
-
Example: https://anabeldg-getaround-pricing-api.hf.space/health
|
| 19 |
-
|
| 20 |
-
### API documentation
|
| 21 |
-
https://anabeldg-getaround-pricing-api.hf.space/docs
|
| 22 |
-
|
| 23 |
-
### Example prediction request
|
| 24 |
-
|
| 25 |
-
```bash
|
| 26 |
-
curl -s -X POST "https://anabeldg-getaround-pricing-api.hf.space/predict" \
|
| 27 |
-
-H "Content-Type: application/json" \
|
| 28 |
-
-d '{"input":[{"model_key":"RENAULT","mileage":50000,"engine_power":110,"fuel":"diesel","paint_color":"black","car_type":"sedan","private_parking_available":true,"has_gps":true,"has_air_conditioning":true,"automatic_car":false,"has_getaround_connect":true,"has_speed_regulator":true,"winter_tires":false}]}' \
|
| 29 |
-
| python -m json.tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs.html
CHANGED
|
@@ -1,55 +1,114 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 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 |
-
{"status":"ok"}</code></pre>
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
"input": [
|
| 54 |
{
|
| 55 |
"model_key": "Citroën",
|
|
@@ -69,43 +128,27 @@
|
|
| 69 |
]
|
| 70 |
}</code></pre>
|
| 71 |
|
| 72 |
-
|
| 73 |
-
<pre><code>{"prediction":[106.2]}</code></pre>
|
| 74 |
|
| 75 |
-
|
| 76 |
-
<pre><code>curl -i -H "Content-Type: application/json" -X POST \
|
| 77 |
-
-d '{"input":[{"model_key":"Citroën","mileage":140411,"engine_power":100,"fuel":"diesel","paint_color":"black","car_type":"convertible","private_parking_available":true,"has_gps":true,"has_air_conditioning":false,"automatic_car":false,"has_getaround_connect":true,"has_speed_regulator":true,"winter_tires":true}]}' \
|
| 78 |
-
http://127.0.0.1:8000/predict</code></pre>
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
"mileage": 140411,
|
| 87 |
-
"engine_power": 100,
|
| 88 |
-
"fuel": "diesel",
|
| 89 |
-
"paint_color": "black",
|
| 90 |
-
"car_type": "convertible",
|
| 91 |
-
"private_parking_available": True,
|
| 92 |
-
"has_gps": True,
|
| 93 |
-
"has_air_conditioning": False,
|
| 94 |
-
"automatic_car": False,
|
| 95 |
-
"has_getaround_connect": True,
|
| 96 |
-
"has_speed_regulator": True,
|
| 97 |
-
"winter_tires": True
|
| 98 |
-
}]
|
| 99 |
-
}
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
| 111 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
| 6 |
+
<title>Getaround — Pricing Optimization API</title>
|
| 7 |
+
<style>
|
| 8 |
+
body { font-family: Arial, sans-serif; margin: 40px; max-width: 900px; }
|
| 9 |
+
h1, h2, h3 { color: #222; }
|
| 10 |
+
code { background: #f4f4f4; padding: 2px 4px; border-radius: 4px; }
|
| 11 |
+
pre { background: #f4f4f4; padding: 12px; border-radius: 6px; overflow-x: auto; }
|
| 12 |
+
.note { background: #eef6ff; padding: 12px; border-left: 4px solid #3b82f6; margin: 20px 0; }
|
| 13 |
+
</style>
|
| 14 |
+
</head>
|
| 15 |
+
|
| 16 |
+
<body>
|
| 17 |
+
|
| 18 |
+
<h1>Getaround — Pricing Optimization API</h1>
|
| 19 |
+
|
| 20 |
+
<p>
|
| 21 |
+
This API exposes a machine learning model that predicts
|
| 22 |
+
<strong>rental_price_per_day</strong> for a car based on its characteristics.
|
| 23 |
+
</p>
|
| 24 |
+
|
| 25 |
+
<hr>
|
| 26 |
+
|
| 27 |
+
<h2>Health Check</h2>
|
| 28 |
+
|
| 29 |
+
<p><strong>GET /health</strong></p>
|
| 30 |
+
|
| 31 |
+
<pre><code>curl -s http://127.0.0.1:8000/health</code></pre>
|
| 32 |
+
|
| 33 |
+
<p>Response:</p>
|
| 34 |
+
|
| 35 |
+
<pre><code>{"status":"ok"}</code></pre>
|
| 36 |
+
|
| 37 |
+
<hr>
|
| 38 |
+
|
| 39 |
+
<h2>Prediction Endpoint</h2>
|
| 40 |
+
|
| 41 |
+
<p><strong>POST /predict</strong></p>
|
| 42 |
+
|
| 43 |
+
<p>
|
| 44 |
+
Returns predicted daily rental prices for one or multiple cars.
|
| 45 |
+
</p>
|
| 46 |
+
|
| 47 |
+
<h3>Accepted Input Format (JSON body)</h3>
|
| 48 |
+
|
| 49 |
+
<ul>
|
| 50 |
+
<li>
|
| 51 |
+
<strong>Matrix format (required by evaluation)</strong>:
|
| 52 |
+
<code>{"input": [[...], [...]]}</code>
|
| 53 |
+
</li>
|
| 54 |
+
<li>
|
| 55 |
+
<strong>Dict format (optional / backward compatible)</strong>:
|
| 56 |
+
<code>{"input": [{"feature": value, ...}]}</code>
|
| 57 |
+
</li>
|
| 58 |
+
</ul>
|
| 59 |
+
|
| 60 |
+
<div class="note">
|
| 61 |
+
<strong>Matrix format requirements:</strong><br><br>
|
| 62 |
+
Each row must contain <strong>exactly 13 values</strong> in the following strict order:
|
| 63 |
+
<br><br>
|
| 64 |
+
<code>
|
| 65 |
+
model_key, mileage, engine_power, fuel, paint_color, car_type,
|
| 66 |
+
private_parking_available, has_gps, has_air_conditioning, automatic_car,
|
| 67 |
+
has_getaround_connect, has_speed_regulator, winter_tires
|
| 68 |
+
</code>
|
| 69 |
+
<br><br>
|
| 70 |
+
Types:
|
| 71 |
+
<ul>
|
| 72 |
+
<li><strong>model_key, fuel, paint_color, car_type</strong> → strings</li>
|
| 73 |
+
<li><strong>mileage, engine_power</strong> → numeric values</li>
|
| 74 |
+
<li>All remaining features → 0 or 1</li>
|
| 75 |
+
</ul>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<hr>
|
| 79 |
+
|
| 80 |
+
<h3>Example — Matrix Format (Required)</h3>
|
| 81 |
+
|
| 82 |
+
<pre><code>{
|
| 83 |
+
"input": [
|
| 84 |
+
["Citroën", 140411, 100, "diesel", "black", "convertible", 1, 1, 0, 0, 1, 1, 1]
|
| 85 |
+
]
|
| 86 |
+
}</code></pre>
|
| 87 |
+
|
| 88 |
+
<h4>curl</h4>
|
| 89 |
+
|
| 90 |
+
<pre><code>curl -X POST "http://127.0.0.1:8000/predict" \
|
| 91 |
+
-H "Content-Type: application/json" \
|
| 92 |
+
-d '{"input":[["Citroën",140411,100,"diesel","black","convertible",1,1,0,0,1,1,1]]}'</code></pre>
|
| 93 |
+
|
| 94 |
+
<h4>Python</h4>
|
| 95 |
+
|
| 96 |
+
<pre><code>import requests
|
| 97 |
+
|
| 98 |
+
payload = {
|
| 99 |
+
"input": [
|
| 100 |
+
["Citroën", 140411, 100, "diesel", "black", "convertible", 1, 1, 0, 0, 1, 1, 1]
|
| 101 |
+
]
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
r = requests.post("http://127.0.0.1:8000/predict", json=payload)
|
| 105 |
+
print(r.json())</code></pre>
|
| 106 |
+
|
| 107 |
+
<hr>
|
| 108 |
+
|
| 109 |
+
<h3>Optional — Dict Format (Backward Compatible)</h3>
|
| 110 |
+
|
| 111 |
+
<pre><code>{
|
| 112 |
"input": [
|
| 113 |
{
|
| 114 |
"model_key": "Citroën",
|
|
|
|
| 128 |
]
|
| 129 |
}</code></pre>
|
| 130 |
|
| 131 |
+
<hr>
|
|
|
|
| 132 |
|
| 133 |
+
<h3>Response Format</h3>
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
<pre><code>{
|
| 136 |
+
"prediction": [97.15]
|
| 137 |
+
}</code></pre>
|
| 138 |
|
| 139 |
+
<p>
|
| 140 |
+
If multiple rows are provided, the response will contain one prediction per row.
|
| 141 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
<hr>
|
| 144 |
+
|
| 145 |
+
<h2>Notes</h2>
|
| 146 |
+
|
| 147 |
+
<ul>
|
| 148 |
+
<li>Unknown categorical values are handled automatically by the model.</li>
|
| 149 |
+
<li>Invalid matrix shape (wrong number of values) will return a 422 error.</li>
|
| 150 |
+
<li>The API is designed for batch predictions.</li>
|
| 151 |
+
</ul>
|
| 152 |
+
|
| 153 |
+
</body>
|
| 154 |
</html>
|
main.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 3 |
-
from pydantic import BaseModel
|
| 4 |
-
from typing import List, Any, Dict, Union
|
| 5 |
-
import pandas as pd
|
| 6 |
-
import joblib
|
| 7 |
-
from pathlib import Path
|
| 8 |
|
| 9 |
|
| 10 |
app = FastAPI(
|
| 11 |
title="Getaround API",
|
| 12 |
version="1.0.0",
|
| 13 |
docs_url=None,
|
| 14 |
-
redoc_url=None
|
| 15 |
)
|
| 16 |
|
| 17 |
@app.get("/", response_class=HTMLResponse)
|
|
@@ -25,56 +27,77 @@ def home():
|
|
| 25 |
|
| 26 |
APP_DIR = Path(__file__).resolve().parent
|
| 27 |
MODEL_PATH = APP_DIR / "pricing_model.joblib"
|
| 28 |
-
|
| 29 |
-
FEATURE_ORDER = [
|
| 30 |
-
"model_key",
|
| 31 |
-
"mileage",
|
| 32 |
-
"engine_power",
|
| 33 |
-
"fuel",
|
| 34 |
-
"paint_color",
|
| 35 |
-
"car_type",
|
| 36 |
-
"private_parking_available",
|
| 37 |
-
"has_gps",
|
| 38 |
-
"has_air_conditioning",
|
| 39 |
-
"automatic_car",
|
| 40 |
-
"has_getaround_connect",
|
| 41 |
-
"has_speed_regulator",
|
| 42 |
-
"winter_tires",
|
| 43 |
-
]
|
| 44 |
|
| 45 |
model = joblib.load(MODEL_PATH)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
class PredictIn(BaseModel):
|
| 48 |
-
input: List[Any] # accepts list[dict] OR list[list] (see docs)
|
| 49 |
|
| 50 |
@app.get("/health")
|
| 51 |
def health():
|
| 52 |
return {"status": "ok"}
|
| 53 |
|
| 54 |
-
|
| 55 |
-
def
|
| 56 |
rows = payload.input
|
| 57 |
|
| 58 |
-
# Accept either:
|
| 59 |
-
# 1) list[dict] with keys matching FEATURE_ORDER
|
| 60 |
-
# 2) list[list] with values in FEATURE_ORDER order
|
| 61 |
if len(rows) == 0:
|
| 62 |
-
return
|
| 63 |
|
| 64 |
first = rows[0]
|
| 65 |
-
if isinstance(first, dict):
|
| 66 |
-
X = pd.DataFrame(rows)
|
| 67 |
-
# Ensure column order & missing columns are handled
|
| 68 |
-
for c in FEATURE_ORDER:
|
| 69 |
-
if c not in X.columns:
|
| 70 |
-
X[c] = None
|
| 71 |
-
X = X[FEATURE_ORDER]
|
| 72 |
-
else:
|
| 73 |
-
# list-like
|
| 74 |
-
X = pd.DataFrame(rows, columns=FEATURE_ORDER)
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
preds = model.predict(X)
|
| 77 |
-
return {"prediction":
|
| 78 |
|
| 79 |
|
| 80 |
BASE_DIR = Path(__file__).resolve().parent
|
|
@@ -82,4 +105,4 @@ BASE_DIR = Path(__file__).resolve().parent
|
|
| 82 |
@app.get("/docs", response_class=HTMLResponse)
|
| 83 |
def docs():
|
| 84 |
html = (BASE_DIR / "docs.html").read_text(encoding="utf-8")
|
| 85 |
-
return HTMLResponse(content=html)
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Any, Dict, List, Union
|
| 4 |
+
|
| 5 |
+
import joblib
|
| 6 |
+
import pandas as pd
|
| 7 |
from fastapi import FastAPI
|
| 8 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 9 |
+
from pydantic import BaseModel, Field, ValidationError
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
app = FastAPI(
|
| 13 |
title="Getaround API",
|
| 14 |
version="1.0.0",
|
| 15 |
docs_url=None,
|
| 16 |
+
redoc_url=None,
|
| 17 |
)
|
| 18 |
|
| 19 |
@app.get("/", response_class=HTMLResponse)
|
|
|
|
| 27 |
|
| 28 |
APP_DIR = Path(__file__).resolve().parent
|
| 29 |
MODEL_PATH = APP_DIR / "pricing_model.joblib"
|
| 30 |
+
FEATURE_ORDER_PATH = APP_DIR / "feature_order.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
model = joblib.load(MODEL_PATH)
|
| 33 |
+
FEATURE_ORDER: List[str] = json.loads(FEATURE_ORDER_PATH.read_text(encoding="utf-8"))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PredictMatrix(BaseModel):
|
| 37 |
+
input: List[List[Any]] = Field(
|
| 38 |
+
...,
|
| 39 |
+
description="List of rows; each row is a list of feature values in the fixed FEATURE_ORDER.",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class PredictDicts(BaseModel):
|
| 44 |
+
input: List[Dict[str, Any]] = Field(
|
| 45 |
+
...,
|
| 46 |
+
description="List of objects with feature names as keys (backward compatibility).",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
PredictPayload = Union[PredictMatrix, PredictDicts]
|
| 50 |
|
|
|
|
|
|
|
| 51 |
|
| 52 |
@app.get("/health")
|
| 53 |
def health():
|
| 54 |
return {"status": "ok"}
|
| 55 |
|
| 56 |
+
|
| 57 |
+
def _payload_to_df(payload: PredictPayload) -> pd.DataFrame:
|
| 58 |
rows = payload.input
|
| 59 |
|
|
|
|
|
|
|
|
|
|
| 60 |
if len(rows) == 0:
|
| 61 |
+
return pd.DataFrame(columns=FEATURE_ORDER)
|
| 62 |
|
| 63 |
first = rows[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
|
| 66 |
+
if isinstance(first, list):
|
| 67 |
+
return pd.DataFrame(rows, columns=FEATURE_ORDER)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
X = pd.DataFrame(rows)
|
| 71 |
+
missing = [c for c in FEATURE_ORDER if c not in X.columns]
|
| 72 |
+
if missing:
|
| 73 |
+
raise ValueError(f"Missing columns: {missing}")
|
| 74 |
+
return X[FEATURE_ORDER]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.post("/predict")
|
| 78 |
+
def predict(payload: ...):
|
| 79 |
+
|
| 80 |
+
N = len(FEATURE_ORDER)
|
| 81 |
+
|
| 82 |
+
rows = payload.input
|
| 83 |
+
if rows and isinstance(rows[0], list):
|
| 84 |
+
bad = [i for i, row in enumerate(rows) if len(row) != N]
|
| 85 |
+
if bad:
|
| 86 |
+
got = [len(rows[i]) for i in bad[:5]]
|
| 87 |
+
raise HTTPException(
|
| 88 |
+
status_code=422,
|
| 89 |
+
detail=(
|
| 90 |
+
f"Invalid input shape. Each row must have exactly {N} values "
|
| 91 |
+
f"(FEATURE_ORDER). Bad rows indices: {bad[:10]}. "
|
| 92 |
+
f"Example bad lengths: {got}. "
|
| 93 |
+
f"Expected order: {FEATURE_ORDER}"
|
| 94 |
+
),
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
X = payload_to_df(payload)
|
| 99 |
preds = model.predict(X)
|
| 100 |
+
return {"prediction": preds.tolist()}
|
| 101 |
|
| 102 |
|
| 103 |
BASE_DIR = Path(__file__).resolve().parent
|
|
|
|
| 105 |
@app.get("/docs", response_class=HTMLResponse)
|
| 106 |
def docs():
|
| 107 |
html = (BASE_DIR / "docs.html").read_text(encoding="utf-8")
|
| 108 |
+
return HTMLResponse(content=html)
|
requirements.txt
CHANGED
|
@@ -6,3 +6,4 @@ scikit-learn==1.4.2
|
|
| 6 |
joblib==1.4.2
|
| 7 |
streamlit==1.36.0
|
| 8 |
openpyxl==3.1.5
|
|
|
|
|
|
| 6 |
joblib==1.4.2
|
| 7 |
streamlit==1.36.0
|
| 8 |
openpyxl==3.1.5
|
| 9 |
+
mlflow==2.7.0
|