get-around-api / api /app.py
klakenyuo's picture
nginx
f275c5a
Raw
History Blame Contribute Delete
12.7 kB
import mlflow
import uvicorn
import pandas as pd
from pydantic import BaseModel, Field, validator
from typing import Literal, List, Union
from fastapi import FastAPI, HTTPException, File, UploadFile
import joblib
import numpy as np
import logging
from pathlib import Path
import os
import traceback
import json
from datetime import datetime
import tempfile # ✅ ajouté pour logs temporaire
# ✅ Dossier temporaire pour les logs (portable)
log_dir = Path(tempfile.gettempdir()) / 'logs'
log_dir.mkdir(exist_ok=True)
# Configuration du fichier de log
log_file = log_dir / f'api_{datetime.now().strftime("%Y%m%d")}.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# Initialisation de l'application FastAPI
app = FastAPI(
title="Car Price Prediction API",
root_path="/api",
description="""
Voici un exemple de requête avec des données réelles :
```json
{
"model_key": "renault",
"mileage": 45000,
"engine_power": 90,
"fuel": "diesel",
"paint_color": "grey",
"car_type": "hatchback",
"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
}
```
Réponse attendue :
```json
{
"prix": 12500.50,
"confidence": 0.95
}
```
""",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# Définition des modèles de données avec validation
class ListIn(BaseModel):
"""
Modèle de données pour les entrées de prédiction
"""
model_key: str = Field(..., description="Modèle de la voiture (ex: renault, citroen, peugeot)")
mileage: int = Field(..., ge=0, description="Kilométrage de la voiture (ex: 45000)")
engine_power: int = Field(..., ge=0, description="Puissance du moteur en chevaux (ex: 90)")
fuel: str = Field(..., description="Type de carburant (diesel, petrol, hybrid, electric)")
paint_color: str = Field(..., description="Couleur de la voiture (ex: grey, black, white)")
car_type: str = Field(..., description="Type de voiture (sedan, hatchback, wagon, van, suv)")
private_parking_available: bool = Field(..., description="Disponibilité d'un parking privé")
has_gps: bool = Field(..., description="Présence d'un GPS")
has_air_conditioning: bool = Field(..., description="Présence de la climatisation")
automatic_car: bool = Field(..., description="Transmission automatique")
has_getaround_connect: bool = Field(..., description="Présence de Getaround Connect")
has_speed_regulator: bool = Field(..., description="Présence d'un régulateur de vitesse")
winter_tires: bool = Field(..., description="Présence de pneus hiver")
class Config:
schema_extra = {
"example": {
"model_key": "renault",
"mileage": 45000,
"engine_power": 90,
"fuel": "diesel",
"paint_color": "grey",
"car_type": "hatchback",
"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
}
}
@validator('fuel')
def validate_fuel(cls, v):
allowed_fuels = ['diesel', 'petrol', 'hybrid', 'electric']
if v.lower() not in allowed_fuels:
raise ValueError(f'Le type de carburant doit être l\'un des suivants: {", ".join(allowed_fuels)}')
return v.lower()
class PredictionOut(BaseModel):
"""
Modèle de données pour la sortie de prédiction
"""
prix: float = Field(..., description="Prix prédit de la voiture")
confidence: float = Field(..., description="Niveau de confiance de la prédiction")
class Config:
schema_extra = {
"example": {
"prix": 12500.50,
"confidence": 0.95
}
}
class ErrorResponse(BaseModel):
"""
Modèle de données pour les réponses d'erreur
"""
error: str = Field(..., description="Message d'erreur détaillé")
timestamp: str = Field(..., description="Horodatage de l'erreur")
traceback: str = Field(None, description="Traceback complet de l'erreur")
def log_error(error: Exception, context: str = ""):
"""
Enregistre une erreur dans le fichier de log avec le contexte
"""
error_details = {
"timestamp": datetime.now().isoformat(),
"context": context,
"error_type": type(error).__name__,
"error_message": str(error),
"traceback": traceback.format_exc()
}
logger.error(f"Erreur dans {context}: {str(error)}")
logger.error(f"Traceback complet: {error_details['traceback']}")
return error_details
def get_valid_categories(pipeline):
"""
Récupère les catégories valides du pipeline de prétraitement
"""
try:
# Récupération des catégories valides pour chaque feature catégorielle
categorical_features = pipeline.named_transformers_['cat'].named_steps['encoder'].categories_
feature_names = pipeline.named_transformers_['cat'].get_feature_names_out()
valid_categories = {}
for i, feature in enumerate(feature_names):
if i < len(categorical_features):
valid_categories[feature] = categorical_features[i].tolist()
return valid_categories
except Exception as e:
logger.error(f"Erreur lors de la récupération des catégories valides: {str(e)}")
return None
def load_models():
"""
Charge les modèles ML depuis les fichiers sauvegardés
"""
try:
# Utilisation d'un chemin absolu pour les fichiers de modèle
current_dir = Path(__file__).parent.parent
model_path = current_dir / 'model' / 'finalized_model.sav'
pipeline_path = current_dir / 'model' / 'finalized_prepoc.sav'
logger.info(f"Tentative de chargement des modèles depuis : {model_path}")
if not model_path.exists() or not pipeline_path.exists():
error_msg = f"Les fichiers de modèle sont introuvables. Vérifié dans : {model_path}"
error_details = log_error(FileNotFoundError(error_msg), "load_models")
raise HTTPException(
status_code=500,
detail=error_details
)
loaded_model = joblib.load(model_path)
pipeline = joblib.load(pipeline_path)
# Récupération des catégories valides
valid_categories = get_valid_categories(pipeline)
if valid_categories:
logger.info(f"Catégories valides chargées: {valid_categories}")
return loaded_model, pipeline
except Exception as e:
error_details = log_error(e, "load_models")
raise HTTPException(
status_code=500,
detail=error_details
)
def predict_price(values):
"""
Effectue la prédiction du prix
"""
try:
# Création d'un DataFrame avec les bonnes colonnes
columns = ['model_key', 'mileage', 'engine_power', 'fuel', 'paint_color',
'car_type', 'private_parking_available', 'has_gps',
'has_air_conditioning', 'automatic_car', 'has_getaround_connect',
'has_speed_regulator', 'winter_tires']
# Formatage du model_key (première lettre en majuscule)
values['model_key'] = values['model_key'].capitalize()
# Conversion des valeurs en DataFrame
df = pd.DataFrame([values], columns=columns)
# Chargement et application des modèles
loaded_model, pipeline = load_models()
# Vérification des catégories valides
valid_categories = get_valid_categories(pipeline)
if valid_categories:
# Vérification de la marque de voiture
if 'model_key' in valid_categories and values['model_key'] not in valid_categories['model_key']:
error_msg = f"Marque de voiture non reconnue: {values['model_key']}. Marques valides: {valid_categories['model_key']}"
error_details = log_error(ValueError(error_msg), "predict_price")
raise HTTPException(
status_code=400,
detail=error_details
)
# Vérification du type de voiture
if 'car_type' in valid_categories and values['car_type'] not in valid_categories['car_type']:
error_msg = f"Type de voiture non reconnu: {values['car_type']}. Types valides: {valid_categories['car_type']}"
error_details = log_error(ValueError(error_msg), "predict_price")
raise HTTPException(
status_code=400,
detail=error_details
)
# Vérification de la couleur
if 'paint_color' in valid_categories and values['paint_color'] not in valid_categories['paint_color']:
error_msg = f"Couleur non reconnue: {values['paint_color']}. Couleurs valides: {valid_categories['paint_color']}"
error_details = log_error(ValueError(error_msg), "predict_price")
raise HTTPException(
status_code=400,
detail=error_details
)
# Transformation des données
transformed_data = pipeline.transform(df)
# Prédiction
result = loaded_model.predict(transformed_data)
# Calcul d'un score de confiance simple
confidence = 0.95 # Exemple de score de confiance
return float(result[0]), confidence
except Exception as e:
error_details = log_error(e, "predict_price")
raise HTTPException(
status_code=500,
detail=error_details
)
@app.get("/", tags=["Root"])
async def index():
"""
Point d'entrée principal de l'API
"""
return {
"message": "Bienvenue sur l'API de prédiction de prix des voitures",
"documentation": "/docs",
"version": "1.0.0"
}
@app.get("/categories", tags=["Categories"])
async def get_categories():
"""
Récupère les catégories valides pour les features catégorielles
"""
try:
_, pipeline = load_models()
valid_categories = get_valid_categories(pipeline)
return valid_categories
except Exception as e:
error_details = log_error(e, "get_categories")
raise HTTPException(
status_code=500,
detail=error_details
)
@app.post("/predict", response_model=PredictionOut, tags=["Prediction"])
async def predict(values: ListIn):
"""
Endpoint pour prédire le prix d'une voiture
- Prend en entrée les caractéristiques de la voiture
- Retourne le prix prédit et un score de confiance
"""
try:
# Conversion des valeurs en dictionnaire
input_values = {
'model_key': values.model_key,
'mileage': values.mileage,
'engine_power': values.engine_power,
'fuel': values.fuel,
'paint_color': values.paint_color,
'car_type': values.car_type,
'private_parking_available': values.private_parking_available,
'has_gps': values.has_gps,
'has_air_conditioning': values.has_air_conditioning,
'automatic_car': values.automatic_car,
'has_getaround_connect': values.has_getaround_connect,
'has_speed_regulator': values.has_speed_regulator,
'winter_tires': values.winter_tires
}
prix, confidence = predict_price(input_values)
logger.info(f"Prédiction effectuée avec succès pour le modèle {values.model_key}")
return {
"prix": round(prix, 2),
"confidence": round(confidence, 2)
}
except Exception as e:
error_details = log_error(e, "predict endpoint")
raise HTTPException(
status_code=500,
detail=error_details
)
if __name__=="__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)