Exposes ensemble feature importance as a standalone endpoint per R5.1 spec. Aggregates XGBoost (0.4) + LightGBM (0.35) + CatBoost (0.25) gain when trained boosters are loaded; falls back to the curated heuristic ranking otherwise, so callers can depend on the endpoint during scaffold/heuristic-only runs. - Factored heuristic drivers into a shared constant (_HEURISTIC_DRIVERS) - Added AVMv2FeatureImportanceResponse model (model_version + source + drivers) - Added service.get_feature_importance() public method - Added tests/test_avm_v2.py::test_feature_importance_heuristic (24 total pass) Co-Authored-By: Paperclip <noreply@paperclip.ing>
86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
"""AVM v2 ensemble router — residential property valuation."""
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from app.models.avm_v2 import (
|
|
ABComparisonRequest,
|
|
ABComparisonResponse,
|
|
AVMv2FeatureImportanceResponse,
|
|
AVMv2ModelInfo,
|
|
AVMv2PredictRequest,
|
|
AVMv2PredictResponse,
|
|
AVMv2RollbackRequest,
|
|
AVMv2TrainRequest,
|
|
AVMv2TrainResponse,
|
|
)
|
|
from app.services.avm_v2_service import avm_v2_service
|
|
|
|
router = APIRouter(prefix="/avm/v2", tags=["AVM v2 Ensemble"])
|
|
|
|
|
|
@router.post("/predict", response_model=AVMv2PredictResponse)
|
|
def predict_v2(req: AVMv2PredictRequest) -> AVMv2PredictResponse:
|
|
"""Predict residential property price using the multi-model ensemble.
|
|
|
|
Ensemble: XGBoost (0.4) + LightGBM (0.35) + CatBoost (0.25).
|
|
Falls back to heuristic when trained models are not available.
|
|
"""
|
|
return avm_v2_service.predict(req)
|
|
|
|
|
|
@router.post("/train", response_model=AVMv2TrainResponse)
|
|
def train_v2(req: AVMv2TrainRequest) -> AVMv2TrainResponse:
|
|
"""Trigger model retraining with Optuna hyperparameter optimization.
|
|
|
|
Loads training data from the model directory, runs Optuna for each
|
|
model in the ensemble, saves versioned artifacts, and registers
|
|
the new version in the model registry.
|
|
"""
|
|
return avm_v2_service.train(req)
|
|
|
|
|
|
@router.post("/compare-v1", response_model=ABComparisonResponse)
|
|
def compare_v1(req: ABComparisonRequest) -> ABComparisonResponse:
|
|
"""Compare v1 (single-model) vs v2 (ensemble) predictions side by side.
|
|
|
|
Runs both models on the same property and returns price difference,
|
|
confidence delta, and a recommendation on which to prefer.
|
|
"""
|
|
return avm_v2_service.compare_v1(req)
|
|
|
|
|
|
@router.get("/model-info", response_model=AVMv2ModelInfo)
|
|
def model_info_v2() -> AVMv2ModelInfo:
|
|
"""Get current active ensemble model information."""
|
|
return avm_v2_service.get_model_info()
|
|
|
|
|
|
@router.get("/feature-importance", response_model=AVMv2FeatureImportanceResponse)
|
|
def feature_importance_v2() -> AVMv2FeatureImportanceResponse:
|
|
"""Global feature importance for the active ensemble.
|
|
|
|
Aggregates XGBoost gain (0.4) + LightGBM gain (0.35) + CatBoost importance (0.25)
|
|
when trained boosters are loaded. Falls back to a curated heuristic ranking when
|
|
the service is running without artifacts.
|
|
"""
|
|
return avm_v2_service.get_feature_importance()
|
|
|
|
|
|
@router.get("/versions", response_model=list[AVMv2ModelInfo])
|
|
def list_versions() -> list[AVMv2ModelInfo]:
|
|
"""List all registered model versions with their metrics and status."""
|
|
return avm_v2_service.list_versions()
|
|
|
|
|
|
@router.post("/rollback", response_model=AVMv2ModelInfo)
|
|
def rollback(req: AVMv2RollbackRequest) -> AVMv2ModelInfo:
|
|
"""Rollback to a previously trained model version.
|
|
|
|
Copies the target version's artifacts to the active model directory,
|
|
reloads models, and updates the registry.
|
|
"""
|
|
try:
|
|
return avm_v2_service.rollback(req.target_version)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|