feat(ai-services): add AVM v2 A/B comparison endpoint and tests
Add POST /avm/v2/compare-v1 endpoint that runs both v1 (single-model) and v2 (ensemble) AVM predictions on the same property and returns a side-by-side comparison with price diff, confidence delta, and a recommendation on which model to prefer. - ABComparisonRequest/Response schemas in avm_v2 models - compare_v1() method in AVMv2EnsembleService - 4 new integration tests for the comparison endpoint - All 47 Python tests pass Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -12,12 +12,17 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.models.avm import AVMPredictRequest
|
||||
from app.models.avm_v2 import (
|
||||
ABComparisonRequest,
|
||||
ABComparisonResponse,
|
||||
AVMv1Summary,
|
||||
AVMv2Comparable,
|
||||
AVMv2FeatureImportance,
|
||||
AVMv2ModelInfo,
|
||||
AVMv2PredictRequest,
|
||||
AVMv2PredictResponse,
|
||||
AVMv2Summary,
|
||||
AVMv2TrainRequest,
|
||||
AVMv2TrainResponse,
|
||||
ModelPrediction,
|
||||
@@ -530,6 +535,91 @@ class AVMv2EnsembleService:
|
||||
ab_test_traffic_pct=0.0,
|
||||
)
|
||||
|
||||
# ── A/B comparison ─────────────────────────────────────────
|
||||
|
||||
def compare_v1(self, req: ABComparisonRequest) -> ABComparisonResponse:
|
||||
"""Compare v1 and v2 predictions on the same property."""
|
||||
from app.services.avm_service import avm_service
|
||||
|
||||
# Build v1 request
|
||||
v1_req = AVMPredictRequest(
|
||||
area=req.area_m2,
|
||||
district=req.district,
|
||||
city=req.city,
|
||||
property_type=req.property_type,
|
||||
bedrooms=req.bedrooms or req.rooms,
|
||||
floors=req.floors,
|
||||
frontage=req.frontage,
|
||||
has_legal_paper=req.has_legal_paper,
|
||||
)
|
||||
v1_result = avm_service.predict(v1_req)
|
||||
|
||||
# Build v2 request
|
||||
v2_req = AVMv2PredictRequest(
|
||||
district=req.district,
|
||||
city=req.city,
|
||||
property_type=req.property_type,
|
||||
area_m2=req.area_m2,
|
||||
rooms=req.rooms or req.bedrooms,
|
||||
has_legal_paper=req.has_legal_paper,
|
||||
distance_to_cbd_km=req.distance_to_cbd_km,
|
||||
distance_to_metro_km=req.distance_to_metro_km,
|
||||
flood_zone_risk=req.flood_zone_risk,
|
||||
building_age_years=req.building_age_years,
|
||||
has_elevator=req.has_elevator,
|
||||
has_parking=req.has_parking,
|
||||
has_pool=req.has_pool,
|
||||
renovation_score=req.renovation_score,
|
||||
view_quality=req.view_quality,
|
||||
interior_quality=req.interior_quality,
|
||||
month=req.month,
|
||||
quarter=req.quarter,
|
||||
is_year_end=req.is_year_end,
|
||||
)
|
||||
v2_result = self.predict(v2_req)
|
||||
|
||||
# Compute diffs
|
||||
price_diff = v2_result.estimated_price_vnd - v1_result.estimated_price_vnd
|
||||
price_diff_pct = (
|
||||
(price_diff / v1_result.estimated_price_vnd * 100)
|
||||
if v1_result.estimated_price_vnd > 0
|
||||
else 0.0
|
||||
)
|
||||
confidence_diff = v2_result.confidence - v1_result.confidence
|
||||
|
||||
# Recommendation logic
|
||||
if v2_result.confidence > v1_result.confidence + 0.05:
|
||||
recommendation = "v2 — higher confidence from ensemble model agreement"
|
||||
elif v1_result.confidence > v2_result.confidence + 0.05:
|
||||
recommendation = "v1 — higher confidence, v2 models may disagree on this property"
|
||||
elif abs(price_diff_pct) < 5:
|
||||
recommendation = "Both models agree (< 5% price difference)"
|
||||
else:
|
||||
recommendation = "v2 — richer feature set captures more market factors"
|
||||
|
||||
return ABComparisonResponse(
|
||||
v1=AVMv1Summary(
|
||||
estimated_price_vnd=v1_result.estimated_price_vnd,
|
||||
confidence=v1_result.confidence,
|
||||
price_per_m2=v1_result.price_per_m2,
|
||||
price_range_low=v1_result.price_range_low,
|
||||
price_range_high=v1_result.price_range_high,
|
||||
),
|
||||
v2=AVMv2Summary(
|
||||
estimated_price_vnd=v2_result.estimated_price_vnd,
|
||||
confidence=v2_result.confidence,
|
||||
price_per_m2_vnd=v2_result.price_per_m2_vnd,
|
||||
price_range_low_vnd=v2_result.price_range_low_vnd,
|
||||
price_range_high_vnd=v2_result.price_range_high_vnd,
|
||||
model_version=v2_result.model_version,
|
||||
ensemble_method=v2_result.ensemble_method,
|
||||
),
|
||||
price_diff_vnd=round(price_diff, -3),
|
||||
price_diff_pct=round(price_diff_pct, 2),
|
||||
confidence_diff=round(confidence_diff, 4),
|
||||
recommendation=recommendation,
|
||||
)
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
avm_v2_service = AVMv2EnsembleService()
|
||||
|
||||
Reference in New Issue
Block a user