feat(ai-services): add building_coverage, loading_docks, zoning to industrial AVM

Completes the industrial-specific feature set required for AVM industrial
valuation. Adds heuristic adjustments for all three new features and
4 new tests covering zoning premiums, loading docks, and coverage ratio.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 17:06:27 +07:00
parent 8592fb436c
commit 13bd76ac5d
3 changed files with 115 additions and 8 deletions

View File

@@ -34,6 +34,9 @@ INDUSTRIAL_FEATURE_NAMES = [
"ceiling_height_m",
"floor_load_ton_m2",
"power_capacity_kva",
"building_coverage",
"loading_docks",
"zoning_encoded",
"industry_demand_index",
"fdi_province_musd",
"labor_cost_province_vnd",
@@ -56,6 +59,15 @@ PROPERTY_TYPE_MAP = {
"office_in_park": 5,
}
ZONING_MAP = {
"general_industrial": 0,
"heavy_industrial": 1,
"light_industrial": 2,
"logistics": 3,
"free_trade_zone": 4,
"high_tech": 5,
}
# ── Province-level rent baselines (USD/m²/month) ────────────────
# Based on Vietnamese industrial real estate market data
PROVINCE_BASELINE: dict[str, float] = {
@@ -113,6 +125,9 @@ def _encode_features(req: IndustrialAVMRequest) -> np.ndarray:
req.ceiling_height_m,
req.floor_load_ton_m2,
req.power_capacity_kva,
req.building_coverage,
req.loading_docks,
ZONING_MAP.get(req.zoning.lower(), 0),
req.industry_demand_index,
req.fdi_province_musd,
req.labor_cost_province_vnd,
@@ -253,6 +268,22 @@ class IndustrialAVMService:
floor_load_adj = 1.0 + max(0.0, (req.floor_load_ton_m2 - 2.0) * 0.03)
power_adj = 1.0 + min(0.10, req.power_capacity_kva / 5000 * 0.10)
# Building coverage — higher coverage = more usable space = premium
coverage_adj = 1.0 + max(0.0, (req.building_coverage - 0.4) * 0.15)
# Loading docks — each dock adds a small premium, diminishing returns
docks_adj = 1.0 + min(0.12, req.loading_docks * 0.02)
# Zoning premium — specialized zones command higher rents
zoning_mult = {
"general_industrial": 1.00,
"heavy_industrial": 0.95,
"light_industrial": 1.05,
"logistics": 1.10,
"free_trade_zone": 1.20,
"high_tech": 1.25,
}.get(req.zoning.lower(), 1.0)
# Economic indicators
demand_adj = 1.0 + (req.industry_demand_index - 0.5) * 0.25
fdi_adj = 1.0 + min(0.15, req.fdi_province_musd / 5000 * 0.15)
@@ -280,6 +311,9 @@ class IndustrialAVMService:
* ceiling_adj
* floor_load_adj
* power_adj
* coverage_adj
* docks_adj
* zoning_mult
* demand_adj
* fdi_adj
* labor_adj
@@ -291,14 +325,17 @@ class IndustrialAVMService:
# Heuristic feature importance
drivers = [
FeatureImportance(feature="province_baseline", importance=0.20),
FeatureImportance(feature="property_type", importance=0.15),
FeatureImportance(feature="park_occupancy_rate", importance=0.12),
FeatureImportance(feature="logistics_connectivity_score", importance=0.10),
FeatureImportance(feature="industry_demand_index", importance=0.10),
FeatureImportance(feature="fdi_province_musd", importance=0.08),
FeatureImportance(feature="distance_to_port_km", importance=0.07),
FeatureImportance(feature="area_m2", importance=0.06),
FeatureImportance(feature="province_baseline", importance=0.16),
FeatureImportance(feature="property_type", importance=0.12),
FeatureImportance(feature="zoning", importance=0.11),
FeatureImportance(feature="park_occupancy_rate", importance=0.10),
FeatureImportance(feature="logistics_connectivity_score", importance=0.09),
FeatureImportance(feature="industry_demand_index", importance=0.09),
FeatureImportance(feature="building_coverage", importance=0.07),
FeatureImportance(feature="loading_docks", importance=0.06),
FeatureImportance(feature="fdi_province_musd", importance=0.06),
FeatureImportance(feature="distance_to_port_km", importance=0.05),
FeatureImportance(feature="area_m2", importance=0.05),
]
return IndustrialAVMResponse(