madamanastasia commited on
Commit
aea70bb
·
1 Parent(s): e66b965

Fix predict matrix format + update docs

Browse files
Files changed (4) hide show
  1. README.md +0 -29
  2. docs.html +128 -85
  3. main.py +65 -42
  4. 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
- <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: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif; line-height: 1.5; margin: 0; }
9
- header { padding: 32px 20px; border-bottom: 1px solid #eee; }
10
- main { max-width: 980px; margin: 0 auto; padding: 20px; }
11
- h1 { margin: 0 0 8px 0; font-size: 28px; }
12
- h2 { margin-top: 24px; font-size: 20px; }
13
- p { margin: 8px 0; }
14
- code, pre { font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; }
15
- pre { background: #f7f7f8; padding: 14px; border-radius: 10px; overflow-x: auto; }
16
- .endpoint { padding: 14px; border: 1px solid #eee; border-radius: 12px; margin: 14px 0; }
17
- .method { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; }
18
- .get { background: #e8f5e9; }
19
- .post { background: #e3f2fd; }
20
- .path { font-weight: 800; margin-left: 8px; }
21
- .note { background: #fff8e1; border: 1px solid #ffe0b2; padding: 10px 12px; border-radius: 10px; margin-top: 10px; }
22
- </style>
23
- </head>
24
- <body>
25
- <header>
26
- <h1>Getaround — Pricing Optimization API</h1>
27
- <p>This API exposes a pricing model that predicts <code>rental_price_per_day</code> for a car based on its characteristics.</p>
28
- </header>
29
-
30
- <main>
31
- <div class="endpoint">
32
- <div><span class="method get">GET</span><span class="path">/health</span></div>
33
- <p>Healthcheck endpoint.</p>
34
- <pre><code>curl -s http://127.0.0.1:8000/health
35
- {"status":"ok"}</code></pre>
36
- </div>
37
-
38
- <div class="endpoint">
39
- <div><span class="method post">POST</span><span class="path">/predict</span></div>
40
- <p>Returns the predicted daily rental price for one or many cars.</p>
41
-
42
- <p><strong>Accepted input format</strong> (JSON body):</p>
43
- <ul>
44
- <li><strong>List of dicts (recommended)</strong>: each dict contains the car fields.</li>
45
- </ul>
46
-
47
- <div class="note">
48
- <strong>Note:</strong> If you want to support <code>list-of-lists</code> inputs, you must keep a strict feature order and validate the length.
49
- </div>
50
-
51
- <p><strong>Example (dict format):</strong></p>
52
- <pre><code>{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  "input": [
54
  {
55
  "model_key": "Citroën",
@@ -69,43 +128,27 @@
69
  ]
70
  }</code></pre>
71
 
72
- <p><strong>Example response:</strong></p>
73
- <pre><code>{"prediction":[106.2]}</code></pre>
74
 
75
- <p><strong>Example (curl):</strong></p>
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
- <p><strong>Example (python):</strong></p>
81
- <pre><code>import requests
 
82
 
83
- payload = {
84
- "input": [{
85
- "model_key": "Citroën",
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
- r = requests.post("http://127.0.0.1:8000/predict", json=payload)
102
- print(r.json())</code></pre>
103
- </div>
104
-
105
- <div class="endpoint">
106
- <div><span class="method get">GET</span><span class="path">/docs</span></div>
107
- <p>Returns this documentation page.</p>
108
- </div>
109
- </main>
110
- </body>
 
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
- @app.post("/predict")
55
- def predict(payload: PredictIn):
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 JSONResponse({"prediction": []})
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": [float(p) for p in preds]}
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