AichaFa commited on
Commit
bef3004
·
0 Parent(s):

Deploy: Code files only

Browse files
Files changed (3) hide show
  1. Dockerfile +37 -0
  2. app.py +403 -0
  3. requirements.txt +9 -0
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy API to Hugging Face Spaces
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ paths:
7
+ - '5-Deploiement/getaround_project/api/**'
8
+
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Push to Hugging Face
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: |
20
+ git config --global user.email "204518881+AichaFa@users.noreply.github.com"
21
+ git config --global user.name "AichaFa"
22
+
23
+ # Clonage dans un dossier temporaire dédié
24
+ git clone https://AichaFa:$HF_TOKEN@huggingface.co/spaces/AichaFaHugFace/getaround-api hf_space
25
+
26
+ # Nettoyage et copie propre
27
+ find hf_space -maxdepth 1 -not -name 'hf_space' -not -name '.git' -exec rm -rf {} \;
28
+ cp -r 5-Deploiement/getaround_project/api/* hf_space/
29
+
30
+ # Push classique sur l'historique linéaire
31
+ cd hf_space
32
+ git add .
33
+ git diff-index --quiet HEAD || git commit -m "Deploy: Synchronisation automatique de l'API"
34
+ git push origin main
35
+ # Force build v5
36
+
37
+
app.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API de prédiction du prix de location - Projet Getaround
3
+ ========================================================
4
+ Cette API expose un point de terminaison /predict qui renvoie le prix
5
+ de location journalier suggéré pour un véhicule, à partir de ses
6
+ caractéristiques.
7
+
8
+ Auteur : Projet déploiement
9
+ """
10
+
11
+ from typing import List, Union
12
+ from pathlib import Path
13
+
14
+ import joblib
15
+ import numpy as np
16
+ import pandas as pd
17
+ from fastapi import FastAPI, HTTPException
18
+ from fastapi.responses import HTMLResponse
19
+ from pydantic import BaseModel, Field
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Configuration de l'application
23
+ # ---------------------------------------------------------------------------
24
+ APP_TITLE = "Getaround - API de prédiction du prix de location"
25
+ APP_DESCRIPTION = (
26
+ "Cette API expose un modèle de Machine Learning entraîné pour suggérer "
27
+ "le prix journalier optimal d'une location de véhicule, à partir de ses "
28
+ "caractéristiques techniques et de ses équipements."
29
+ )
30
+ APP_VERSION = "1.0.0"
31
+
32
+ MODEL_PATH = Path(__file__).resolve().parent / "best_model.joblib"
33
+
34
+ # Colonnes attendues dans l'ordre du modèle
35
+ FEATURE_COLUMNS = [
36
+ "model_key", "mileage", "engine_power", "fuel", "paint_color",
37
+ "car_type", "private_parking_available", "has_gps",
38
+ "has_air_conditioning", "automatic_car", "has_getaround_connect",
39
+ "has_speed_regulator", "winter_tires"
40
+ ]
41
+
42
+ # Modalités acceptées (à titre informatif)
43
+ ACCEPTED_MODELS = [
44
+ "Citroën", "Renault", "BMW", "Peugeot", "Audi", "Nissan", "Mitsubishi",
45
+ "Mercedes", "Volkswagen", "Toyota", "SEAT", "Subaru", "Opel", "PGO",
46
+ "Ferrari", "Other"
47
+ ]
48
+ ACCEPTED_FUELS = ["diesel", "petrol", "hybrid_petrol", "electro"]
49
+ ACCEPTED_COLORS = ["black", "grey", "white", "red", "silver", "blue",
50
+ "orange", "beige", "brown", "green"]
51
+ ACCEPTED_CAR_TYPES = ["convertible", "coupe", "estate", "hatchback",
52
+ "sedan", "subcompact", "suv", "van"]
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Initialisation
56
+ # ---------------------------------------------------------------------------
57
+ app = FastAPI(
58
+ title=APP_TITLE,
59
+ description=APP_DESCRIPTION,
60
+ version=APP_VERSION,
61
+ docs_url=None, # on remplace par notre propre /docs
62
+ redoc_url="/redoc"
63
+ )
64
+
65
+ # Chargement du modèle une fois au démarrage
66
+ try:
67
+ model = joblib.load(MODEL_PATH)
68
+ MODEL_LOADED = True
69
+ except Exception as exc:
70
+ print(f"Erreur de chargement du modèle : {exc}")
71
+ model = None
72
+ MODEL_LOADED = False
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Schémas Pydantic
77
+ # ---------------------------------------------------------------------------
78
+ class PredictionInput(BaseModel):
79
+ """Schéma d'entrée acceptant une liste de listes de caractéristiques.
80
+
81
+ Ordre attendu des colonnes :
82
+ [model_key, mileage, engine_power, fuel, paint_color, car_type,
83
+ private_parking_available, has_gps, has_air_conditioning,
84
+ automatic_car, has_getaround_connect, has_speed_regulator,
85
+ winter_tires]
86
+ """
87
+ input: List[List[Union[str, int, float, bool]]] = Field(
88
+ ...,
89
+ examples=[[[
90
+ "Citroën", 140411, 100, "diesel", "black", "convertible",
91
+ True, True, False, False, True, True, True
92
+ ]]]
93
+ )
94
+
95
+
96
+ class PredictionOutput(BaseModel):
97
+ prediction: List[float]
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Endpoints
102
+ # ---------------------------------------------------------------------------
103
+ @app.get("/", tags=["Accueil"])
104
+ async def root():
105
+ """Point d'entrée par défaut. Vérifie que l'API est opérationnelle."""
106
+ return {
107
+ "message": "API Getaround opérationnelle",
108
+ "version": APP_VERSION,
109
+ "model_loaded": MODEL_LOADED,
110
+ "documentation": "/docs"
111
+ }
112
+
113
+
114
+ @app.get("/health", tags=["Accueil"])
115
+ async def health():
116
+ """Indicateur de santé du service."""
117
+ return {"status": "ok" if MODEL_LOADED else "model_missing"}
118
+
119
+
120
+ @app.post("/predict",
121
+ tags=["Prédiction"],
122
+ response_model=PredictionOutput,
123
+ summary="Prédire le prix de location journalier")
124
+ async def predict(payload: PredictionInput):
125
+ """Renvoie la prédiction du prix de location journalier (EUR/jour).
126
+
127
+ Le format d'entrée attendu est conforme à la spécification :
128
+ une liste de listes contenant, dans l'ordre, les caractéristiques du
129
+ véhicule.
130
+
131
+ Exemple :
132
+ {
133
+ "input": [["Citroën", 140411, 100, "diesel", "black",
134
+ "convertible", true, true, false, false, true,
135
+ true, true]]
136
+ }
137
+
138
+ Renvoie :
139
+ {"prediction": [108.42]}
140
+ """
141
+ if not MODEL_LOADED:
142
+ raise HTTPException(status_code=503,
143
+ detail="Modèle non chargé sur le serveur.")
144
+
145
+ try:
146
+ df_input = pd.DataFrame(payload.input, columns=FEATURE_COLUMNS)
147
+
148
+ # Conversion explicite des booléens
149
+ bool_cols = ["private_parking_available", "has_gps",
150
+ "has_air_conditioning", "automatic_car",
151
+ "has_getaround_connect", "has_speed_regulator",
152
+ "winter_tires"]
153
+ for c in bool_cols:
154
+ df_input[c] = df_input[c].astype(int)
155
+
156
+ # Conversion des numériques
157
+ df_input["mileage"] = df_input["mileage"].astype(int)
158
+ df_input["engine_power"] = df_input["engine_power"].astype(int)
159
+
160
+ preds = model.predict(df_input)
161
+ return {"prediction": [round(float(p), 2) for p in preds]}
162
+
163
+ except Exception as exc:
164
+ raise HTTPException(status_code=400,
165
+ detail=f"Erreur lors de la prédiction : {exc}")
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Documentation HTML personnalisée à /docs
170
+ # ---------------------------------------------------------------------------
171
+ @app.get("/docs", response_class=HTMLResponse, include_in_schema=False)
172
+ async def custom_docs():
173
+ """Page de documentation HTML stylée."""
174
+ html = """
175
+ <!DOCTYPE html>
176
+ <html lang="fr">
177
+ <head>
178
+ <meta charset="UTF-8">
179
+ <title>Documentation - API Getaround</title>
180
+ <style>
181
+ * { box-sizing: border-box; margin: 0; padding: 0; }
182
+ body {
183
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
184
+ "Helvetica Neue", Arial, sans-serif;
185
+ background: #f5f7fb;
186
+ color: #1f2937;
187
+ line-height: 1.6;
188
+ }
189
+ .container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
190
+ header {
191
+ background: linear-gradient(135deg, #5b21b6 0%, #7c3aed 100%);
192
+ color: white;
193
+ padding: 48px 24px;
194
+ text-align: center;
195
+ border-radius: 12px;
196
+ margin-bottom: 32px;
197
+ box-shadow: 0 8px 24px rgba(91, 33, 182, 0.18);
198
+ }
199
+ h1 { font-size: 2.4em; margin-bottom: 8px; font-weight: 700; }
200
+ header p { font-size: 1.05em; opacity: 0.95; }
201
+ h2 {
202
+ color: #5b21b6;
203
+ margin: 32px 0 16px 0;
204
+ font-size: 1.5em;
205
+ border-bottom: 2px solid #e9d5ff;
206
+ padding-bottom: 8px;
207
+ }
208
+ h3 { color: #4c1d95; margin: 20px 0 12px 0; font-size: 1.15em; }
209
+ section {
210
+ background: white;
211
+ padding: 28px;
212
+ border-radius: 12px;
213
+ margin-bottom: 24px;
214
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
215
+ }
216
+ .endpoint {
217
+ background: #f9fafb;
218
+ border-left: 4px solid #7c3aed;
219
+ padding: 16px 20px;
220
+ border-radius: 6px;
221
+ margin: 16px 0;
222
+ }
223
+ .method {
224
+ display: inline-block;
225
+ padding: 4px 12px;
226
+ border-radius: 4px;
227
+ font-weight: 700;
228
+ font-size: 0.85em;
229
+ letter-spacing: 0.5px;
230
+ margin-right: 12px;
231
+ }
232
+ .method.get { background: #dbeafe; color: #1e40af; }
233
+ .method.post { background: #dcfce7; color: #166534; }
234
+ code {
235
+ background: #f3f4f6;
236
+ padding: 2px 8px;
237
+ border-radius: 4px;
238
+ font-family: "Monaco", "Consolas", monospace;
239
+ font-size: 0.9em;
240
+ color: #be185d;
241
+ }
242
+ pre {
243
+ background: #1f2937;
244
+ color: #e5e7eb;
245
+ padding: 18px;
246
+ border-radius: 8px;
247
+ overflow-x: auto;
248
+ margin: 12px 0;
249
+ font-size: 0.88em;
250
+ line-height: 1.5;
251
+ }
252
+ pre code { background: none; color: inherit; padding: 0; }
253
+ table {
254
+ width: 100%;
255
+ border-collapse: collapse;
256
+ margin: 12px 0;
257
+ }
258
+ th, td {
259
+ padding: 10px 14px;
260
+ text-align: left;
261
+ border-bottom: 1px solid #e5e7eb;
262
+ }
263
+ th { background: #f3f4f6; font-weight: 600; color: #374151; }
264
+ ul { padding-left: 24px; margin: 8px 0; }
265
+ li { margin: 4px 0; }
266
+ footer {
267
+ text-align: center;
268
+ color: #6b7280;
269
+ font-size: 0.9em;
270
+ margin-top: 32px;
271
+ padding: 16px;
272
+ }
273
+ </style>
274
+ </head>
275
+ <body>
276
+ <div class="container">
277
+ <header>
278
+ <h1>API Getaround - Prédiction du prix de location</h1>
279
+ <p>Documentation des points de terminaison - Version 1.0.0</p>
280
+ </header>
281
+
282
+ <section>
283
+ <h2>Présentation</h2>
284
+ <p>Cette API met à disposition un modèle de Machine Learning
285
+ entraîné pour suggérer le prix journalier optimal d'une location
286
+ de véhicule sur la plateforme Getaround.</p>
287
+ <p>Le modèle sous-jacent est un <strong>XGBoost Regressor</strong>
288
+ avec les performances suivantes sur le jeu de test :</p>
289
+ <ul>
290
+ <li>Erreur absolue moyenne (MAE) : <strong>9,18 EUR</strong></li>
291
+ <li>RMSE : <strong>12,82 EUR</strong></li>
292
+ <li>R² : <strong>0,846</strong></li>
293
+ </ul>
294
+ </section>
295
+
296
+ <section>
297
+ <h2>Points de terminaison</h2>
298
+
299
+ <div class="endpoint">
300
+ <span class="method get">GET</span><code>/</code>
301
+ <h3>Accueil</h3>
302
+ <p>Renvoie un message confirmant que l'API est opérationnelle
303
+ et l'état du chargement du modèle.</p>
304
+ <h4>Exemple de réponse</h4>
305
+ <pre><code>{
306
+ "message": "API Getaround opérationnelle",
307
+ "version": "1.0.0",
308
+ "model_loaded": true,
309
+ "documentation": "/docs"
310
+ }</code></pre>
311
+ </div>
312
+
313
+ <div class="endpoint">
314
+ <span class="method get">GET</span><code>/health</code>
315
+ <h3>Santé du service</h3>
316
+ <p>Renvoie l'état de santé du service. Utile pour les
317
+ vérifications automatiques.</p>
318
+ <pre><code>{"status": "ok"}</code></pre>
319
+ </div>
320
+
321
+ <div class="endpoint">
322
+ <span class="method post">POST</span><code>/predict</code>
323
+ <h3>Prédiction du prix</h3>
324
+ <p>Retourne le prix journalier estimé pour un ou plusieurs
325
+ véhicules, à partir de leurs caractéristiques.</p>
326
+
327
+ <h4>Schéma d'entrée</h4>
328
+ <p>Le corps de la requête est un JSON dont la clé
329
+ <code>input</code> contient une liste de listes. Chaque
330
+ liste interne représente un véhicule et respecte l'ordre
331
+ de colonnes suivant :</p>
332
+ <table>
333
+ <tr><th>Position</th><th>Champ</th><th>Type</th><th>Exemple</th></tr>
334
+ <tr><td>1</td><td>model_key</td><td>chaîne</td><td>"Citroën"</td></tr>
335
+ <tr><td>2</td><td>mileage</td><td>entier</td><td>140411</td></tr>
336
+ <tr><td>3</td><td>engine_power</td><td>entier</td><td>100</td></tr>
337
+ <tr><td>4</td><td>fuel</td><td>chaîne</td><td>"diesel"</td></tr>
338
+ <tr><td>5</td><td>paint_color</td><td>chaîne</td><td>"black"</td></tr>
339
+ <tr><td>6</td><td>car_type</td><td>chaîne</td><td>"convertible"</td></tr>
340
+ <tr><td>7</td><td>private_parking_available</td><td>booléen</td><td>true</td></tr>
341
+ <tr><td>8</td><td>has_gps</td><td>booléen</td><td>true</td></tr>
342
+ <tr><td>9</td><td>has_air_conditioning</td><td>booléen</td><td>false</td></tr>
343
+ <tr><td>10</td><td>automatic_car</td><td>booléen</td><td>false</td></tr>
344
+ <tr><td>11</td><td>has_getaround_connect</td><td>booléen</td><td>true</td></tr>
345
+ <tr><td>12</td><td>has_speed_regulator</td><td>booléen</td><td>true</td></tr>
346
+ <tr><td>13</td><td>winter_tires</td><td>booléen</td><td>true</td></tr>
347
+ </table>
348
+
349
+ <h4>Exemple de requête (cURL)</h4>
350
+ <pre><code>curl -i -H "Content-Type: application/json" -X POST \\
351
+ -d '{"input": [["Citroën", 140411, 100, "diesel", "black",
352
+ "convertible", true, true, false, false, true, true, true]]}' \\
353
+ https://votre-url/predict</code></pre>
354
+
355
+ <h4>Exemple de requête (Python)</h4>
356
+ <pre><code>import requests
357
+
358
+ response = requests.post("https://votre-url/predict", json={
359
+ "input": [["Citroën", 140411, 100, "diesel", "black",
360
+ "convertible", True, True, False, False, True, True, True]]
361
+ })
362
+ print(response.json())</code></pre>
363
+
364
+ <h4>Exemple de réponse</h4>
365
+ <pre><code>{
366
+ "prediction": [108.42]
367
+ }</code></pre>
368
+ </div>
369
+ </section>
370
+
371
+ <section>
372
+ <h2>Modalités acceptées</h2>
373
+ <h3>Marques (model_key)</h3>
374
+ <p>Citroën, Renault, BMW, Peugeot, Audi, Nissan, Mitsubishi,
375
+ Mercedes, Volkswagen, Toyota, SEAT, Subaru, Opel, PGO, Ferrari,
376
+ Other</p>
377
+ <h3>Carburants (fuel)</h3>
378
+ <p>diesel, petrol, hybrid_petrol, electro</p>
379
+ <h3>Couleurs (paint_color)</h3>
380
+ <p>black, grey, white, red, silver, blue, orange, beige, brown,
381
+ green</p>
382
+ <h3>Types de véhicules (car_type)</h3>
383
+ <p>convertible, coupe, estate, hatchback, sedan, subcompact,
384
+ suv, van</p>
385
+ </section>
386
+
387
+ <footer>
388
+ <p>Documentation interactive alternative disponible sur
389
+ <code>/redoc</code></p>
390
+ </footer>
391
+ </div>
392
+ </body>
393
+ </html>
394
+ """
395
+ return HTMLResponse(content=html, status_code=200)
396
+
397
+
398
+ # ---------------------------------------------------------------------------
399
+ # Lancement
400
+ # ---------------------------------------------------------------------------
401
+ if __name__ == "__main__":
402
+ import uvicorn
403
+ uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ scikit-learn==1.5.2
4
+ xgboost==2.1.1
5
+ pandas>=2.2.3
6
+ numpy>=2.0.0
7
+ joblib==1.4.2
8
+ pydantic==2.9.2
9
+ python-multipart==0.0.12