This commit is contained in:
Ole
2026-05-16 06:54:17 +00:00
commit 1399f61c1a
44 changed files with 6746 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
"""Scoring engine for FINN listings enriched with Eiendom.no data."""
import logging
from typing import Any
from .models import EiendomUnit, SimilarUnit
logger = logging.getLogger(__name__)
def _clamp(value: float, min_value: float, max_value: float) -> float:
return max(min_value, min(max_value, value))
def score_market_position(unit: EiendomUnit | None) -> float:
if unit is None or unit.estimated_selling_price is None or unit.listing_price is None:
return 0.0
ratio = unit.listing_price / unit.estimated_selling_price
if ratio <= 0.9:
return 20.0
if ratio <= 1.0:
return 16.0 + (1.0 - ratio) * 40.0
if ratio <= 1.1:
return 12.0 - (ratio - 1.0) * 40.0
return 5.0
def score_economy(ad: Any, unit: EiendomUnit | None) -> float:
if ad.total_price is None:
return 0.0
if unit and unit.estimated_selling_price:
ratio = ad.total_price / unit.estimated_selling_price
if ratio <= 0.95:
return 20.0
if ratio <= 1.0:
return 15.0
if ratio <= 1.05:
return 10.0
return 6.0
if ad.asking_price and ad.total_price <= ad.asking_price:
return 12.0
return 8.0
def score_comparable_sales(listings: list[SimilarUnit], listing_price: int | None) -> float:
if not listings or listing_price is None:
return 0.0
selling_prices = [unit.selling_price for unit in listings if unit.selling_price]
if not selling_prices:
return 0.0
average = sum(selling_prices) / len(selling_prices)
ratio = listing_price / average
score = (1.0 - abs(ratio - 1.0)) * 20.0
return float(_clamp(score, 0.0, 20.0))
def score_location(address: str | None, district: str | None) -> float:
if not address and not district:
return 0.0
if district and "oslo" in district.lower():
return 15.0
if address and "oslo" in address.lower():
return 12.0
return 7.0
def score_layout_and_potential(description: str | None, rooms: int | None) -> float:
score = 0.0
if rooms and rooms >= 4:
score += 10.0
if description and "potensial" in description.lower():
score += 8.0
return float(_clamp(score, 0.0, 20.0))
def score_outdoor_and_view(description: str | None) -> float:
if not description:
return 0.0
score = 5.0 if "utsikt" in description.lower() or "balkong" in description.lower() else 0.0
return float(_clamp(score, 0.0, 15.0))
def score_rental_potential(description: str | None) -> float:
if not description:
return 0.0
score = 10.0 if "hybel" in description.lower() or "leie" in description.lower() else 0.0
return score
def score_renovation_upside(description: str | None, asking_price: int | None) -> float:
score = 0.0
if description and "renover" in description.lower():
score += 10.0
if asking_price and asking_price > 0:
score += 5.0
return float(_clamp(score, 0.0, 15.0))
def score_risk(description: str | None, unit: EiendomUnit | None) -> float:
if unit is None:
return -10.0
if description and "usikker" in description.lower():
return -10.0
return 0.0
def score_ad(
ad: Any, unit: EiendomUnit | None, similar_units: list[SimilarUnit]
) -> dict[str, float]:
scores = {
"economy": score_economy(ad, unit),
"market_position": score_market_position(unit),
"comparable_sales": score_comparable_sales(
similar_units, ad.total_price or ad.asking_price
),
"location": score_location(ad.address, ad.district),
"layout": score_layout_and_potential(ad.listing_description, ad.rooms),
"outdoor": score_outdoor_and_view(ad.listing_description),
"rental_potential": score_rental_potential(ad.listing_description),
"renovation": score_renovation_upside(ad.listing_description, ad.asking_price),
"risk": score_risk(ad.listing_description, unit),
}
scores["total"] = float(_clamp(sum(scores.values()), 0.0, 100.0))
return scores
def classify_ad(scores: dict[str, float]) -> list[str]:
categories: list[str] = []
total = scores.get("total", 0.0)
if total >= 70:
categories.append("bargain_candidate")
if total >= 60:
categories.append("safe_candidate")
if 50 <= total < 70:
categories.append("lifestyle_candidate")
if scores.get("renovation", 0.0) >= 8:
categories.append("renovation_candidate")
if scores.get("rental_potential", 0.0) >= 5:
categories.append("hybel_candidate")
if scores.get("risk", 0.0) < 0:
categories.append("risk_object")
if total < 30:
categories.append("not_interesting")
if 30 <= total < 60:
categories.append("manual_review_required")
return categories