Spaces:
Runtime error
Runtime error
| 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 | |
| } | |
| } | |
| 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 | |
| ) | |
| 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" | |
| } | |
| 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 | |
| ) | |
| 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) | |