| name | npa-comparator |
| description | Cross-NPA comparison engine for Thai distressed properties. Finds comparable NPA properties by GPS proximity, computes area benchmarks (median/p25/p75), and returns type-specific comparison results (condo, land, house, commercial). Condos additionally use external market data from market_adapter when available. Non-condo is cross-NPA only — no external market data exists yet. |
NPA Comparator
Finds comparable NPA properties and benchmarks pricing for any property type.
What It Does
Given an NpaProperty, returns a typed comparison showing:
- How its price ranks vs comparable NPAs in the same area
- Area benchmark (median / p25 / p75) for that property type + district
- Which investment strategies make sense for this type
- Honest
data_quality signal (HIGH / MEDIUM / LOW)
⚠️ Honest Limitations
Non-condo types (land, house, townhouse, commercial, factory) are cross-NPA comparison only.
There is no external market data for these types in the current DB. The comparison signal answers: "Is this NPA cheap relative to other NPAs of the same type in the same area?" — not "Is this NPA cheap relative to market asking prices?"
Condo is the only type with true market comparison (via DDProperty, Hipflat, PropertyHub, ZMyHome).
True market comparison for non-condo requires Phase 2 data sources:
- DDProperty house/land listings (scraper currently indexes condos only)
- Treasury Department land appraisal API (ราคาประเมินที่ดิน)
- Baania.com house/land listings
Usage
import sys, os
sys.path.insert(0, '/path/to/npa-comparator/scripts')
from comparator import compare_npa
from adapter import search
from models import SearchFilters, Source
results = search(SearchFilters(province='กรุงเทพ', sources=[Source.BAM], limit=5))
npa = results[0]
result = compare_npa(npa)
print(result.comparison_type)
print(result.price_position)
print(result.data_quality)
print(result.comparable_count)
print(result.area_benchmark.median)
Per-Type Examples
Condo
from comparator import compare_npa
result = compare_npa(condo_npa)
result.price_per_sqm
result.median_market_sqm
result.discount_vs_market_pct
result.discount_vs_npa_median_pct
result.comparison_basis
Land
result = compare_npa(land_npa)
result.price_per_wa
result.total_area_wa
result.median_area_price_per_wa
result.discount_vs_npa_median_pct
House / Townhouse
result = compare_npa(house_npa)
result.price_per_wa_land
result.total_land_wa
result.median_area_price_per_wa_land
result.price_per_unit
result.building_age_years
result.renovation_estimate
result.renovation_data_caveat
result.usable_area_sqm
result.price_per_sqm_usable
result.usable_area_caveat
Commercial / Factory
result = compare_npa(commercial_npa)
result.price_per_sqm
result.median_area_price_per_sqm
result.property_subtype
result.commercial_density
Strategy Mapping
| Type | Strategies |
|---|
| CONDO | rent, flip, expat_rental |
| LAND | land_bank, develop, subdivide |
| HOUSE | rent, flip, renovate_and_rent |
| TOWNHOUSE | rent, flip, renovate_and_rent |
| COMMERCIAL | lease, owner_occupy, redevelop |
| FACTORY | lease, owner_occupy |
comparison_basis Field
| Value | Meaning |
|---|
"market_verified" | Compared against DDProperty/Hipflat/PropertyHub/ZMyHome market data (condos with project name match only) |
"cross_npa" | Compared against other NPA properties of same type in same area. Not a market price comparison. |
data_quality Field
| Value | Threshold |
|---|
"HIGH" | >20 comps, or ≥5 comps with GPS |
"MEDIUM" | 3–20 comps |
"LOW" | <3 comps or no GPS |
Proximity Search Config
Override defaults per call:
from proximity import ProximityConfig, find_comparables
cfg = ProximityConfig(radius_km=1.0, size_tolerance_pct=0.20, max_results=10)
comps = find_comparables(npa, config=cfg)
Default radii: CONDO 2km, HOUSE/TOWNHOUSE/COMMERCIAL 3km, LAND 5km, FACTORY 10km.
Fallback: district-wide → province-wide when <3 GPS comps found.
Architecture
npa-comparator/
scripts/
models.py — Pydantic v2 comparison models (BaseComparison + type-specific)
proximity.py — GPS bbox → haversine filter → district/province fallback
benchmarks.py — cross-NPA median/p25/p75 per category+district (24h cache)
comparator.py — dispatcher: compare_npa() → typed result
All scripts import from npa-adapter (symlinked from Collector) via sys.path. No raw SQL — uses adapter.search() exclusively.
Phase 2 Roadmap
To enable true market comparison for non-condo:
- DDProperty houses/land — Modify Collector's ddproperty scraper to capture non-condo listings with
property_type field.
- Treasury land appraisal API — Government ราคาประเมินที่ดิน by deed number. Strongest signal for land.
- Baania/Thinkofliving — House project market prices.
Once available, comparator.py condo path pattern can be replicated for each type.