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:
Ho Ngoc Hai
2026-04-16 17:35:30 +07:00
parent 74804757c5
commit a6e53e3d06
4 changed files with 235 additions and 0 deletions

View File

@@ -172,3 +172,75 @@ def test_model_info_v2():
data = resp.json()
assert "model_version" in data
assert data["is_active"] is True
# ── A/B comparison tests ─────────────────────────────────────
_COMPARE_PAYLOAD = {
"district": "Cầu Giấy",
"city": "Hà Nội",
"property_type": "apartment",
"area_m2": 80.0,
"rooms": 2,
"month": 3,
"quarter": 1,
}
def test_compare_v1_returns_both_models():
"""Compare endpoint returns v1 and v2 predictions."""
resp = client.post("/avm/v2/compare-v1", json=_COMPARE_PAYLOAD)
assert resp.status_code == 200
data = resp.json()
assert "v1" in data
assert "v2" in data
assert data["v1"]["estimated_price_vnd"] > 0
assert data["v2"]["estimated_price_vnd"] > 0
assert 0 <= data["v1"]["confidence"] <= 1
assert 0 <= data["v2"]["confidence"] <= 1
def test_compare_v1_returns_diffs():
"""Compare endpoint computes price and confidence differences."""
resp = client.post("/avm/v2/compare-v1", json=_COMPARE_PAYLOAD)
data = resp.json()
expected_diff = data["v2"]["estimated_price_vnd"] - data["v1"]["estimated_price_vnd"]
assert abs(data["price_diff_vnd"] - expected_diff) < 10_000 # rounding tolerance
assert "price_diff_pct" in data
assert isinstance(data["price_diff_pct"], float)
assert "confidence_diff" in data
def test_compare_v1_returns_recommendation():
"""Compare endpoint provides a recommendation string."""
resp = client.post("/avm/v2/compare-v1", json=_COMPARE_PAYLOAD)
data = resp.json()
assert "recommendation" in data
assert len(data["recommendation"]) > 0
def test_compare_v1_with_v2_features():
"""Compare endpoint passes v2-specific features correctly."""
payload = {
**_COMPARE_PAYLOAD,
"distance_to_cbd_km": 5.0,
"distance_to_metro_km": 0.8,
"flood_zone_risk": 0.1,
"building_age_years": 3,
"has_elevator": True,
"has_parking": True,
"renovation_score": 0.9,
"view_quality": 0.8,
"interior_quality": 0.85,
}
resp = client.post("/avm/v2/compare-v1", json=payload)
assert resp.status_code == 200
data = resp.json()
# v2 should capture these extra features
assert data["v2"]["estimated_price_vnd"] > 0
assert data["v2"]["model_version"] is not None