diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-13 18:55:58 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-13 18:55:58 +0200 |
| commit | e92bf0544d423f0ab2ff7801f02b17f863a97387 (patch) | |
| tree | dace83622f146672aa4f2902d4935523e75847e6 | |
| parent | e4fdd8457d2d320eea502f0801fc22eceb8947b1 (diff) | |
| download | housing-e92bf0544d423f0ab2ff7801f02b17f863a97387.tar.zst | |
Update score calculation
| -rw-r--r-- | app/components.js | 83 | ||||
| -rw-r--r-- | app/models.js | 209 |
2 files changed, 208 insertions, 84 deletions
diff --git a/app/components.js b/app/components.js index 1c8c6aa..0995692 100644 --- a/app/components.js +++ b/app/components.js @@ -422,7 +422,7 @@ export class Sidebar { children: [ Dom.heading( 3, - "Scoring", + "Scoring Weights", new DomOptions({ styles: { color: "#333", @@ -431,60 +431,95 @@ export class Sidebar { }, }), ), + // Basic house properties Widgets.slider( "w-price", - "Price weight", + "Price", "price", this.#weights.price, this.#onWeightChange, ), Widgets.slider( + "w-year", + "Construction Year", + "constructionYear", + this.#weights.constructionYear, + this.#onWeightChange, + ), + Widgets.slider( + "w-area", + "Living Area", + "livingArea", + this.#weights.livingArea, + this.#onWeightChange, + ), + + // Location factors + Widgets.slider( "w-market", - "Market distance", + "Market Distance", "distanceMarket", this.#weights.distanceMarket, this.#onWeightChange, ), Widgets.slider( "w-school", - "School distance", + "School Distance", "distanceSchool", this.#weights.distanceSchool, this.#onWeightChange, ), + + // Transit distances Widgets.slider( - "w-crime", - "Crime rate", - "crimeRate", - this.#weights.crimeRate, + "w-train", + "Train Distance", + "distanceTrain", + this.#weights.distanceTrain, this.#onWeightChange, ), Widgets.slider( - "w-safety", - "Safety index", - "safety", - this.#weights.safety, + "w-lightrail", + "Light Rail Distance", + "distanceLightRail", + this.#weights.distanceLightRail, this.#onWeightChange, ), Widgets.slider( - "w-students", - "S2 students", - "s2Students", - this.#weights.s2Students, + "w-tram", + "Tram Distance", + "distanceTram", + this.#weights.distanceTram, this.#onWeightChange, ), + + // Statistical area factors Widgets.slider( - "w-railway", - "Railway distance", - "distanceRailway", - this.#weights.distanceRailway, + "w-foreign", + "Foreign Speakers", + "foreignSpeakers", + this.#weights.foreignSpeakers, this.#onWeightChange, ), Widgets.slider( - "w-year", - "Construction year", - "constructionYear", - this.#weights.constructionYear, + "w-unemployment", + "Unemployment Rate", + "unemploymentRate", + this.#weights.unemploymentRate, + this.#onWeightChange, + ), + Widgets.slider( + "w-income", + "Average Income", + "averageIncome", + this.#weights.averageIncome, + this.#onWeightChange, + ), + Widgets.slider( + "w-education", + "Higher Education", + "higherEducation", + this.#weights.higherEducation, this.#onWeightChange, ), ], diff --git a/app/models.js b/app/models.js index dbca0b1..8cd1b5e 100644 --- a/app/models.js +++ b/app/models.js @@ -236,25 +236,13 @@ export class Geospatial { /** * @param {number} [marketDistance] * @param {number} [schoolDistance] - * @param {number} [crimeRate] - * @param {number} [safetyIndex] - * @param {number} [s2StudentRatio] * @param {number} [railwayDistance] */ - constructor( - marketDistance = 0, - schoolDistance = 0, - crimeRate = 0, - safetyIndex = 0, - s2StudentRatio = 0, - railwayDistance = 0, - ) { + constructor(marketDistance = 0, schoolDistance = 0, railwayDistance = 0) { this.marketDistance = marketDistance; this.schoolDistance = schoolDistance; - this.crimeRate = crimeRate; - this.safetyIndex = safetyIndex; - this.s2StudentRatio = s2StudentRatio; this.railwayDistance = railwayDistance; + // Removed: crimeRate, safetyIndex, s2StudentRatio } /** @param {GeospatialJson} data @returns {Geospatial} */ @@ -262,9 +250,6 @@ export class Geospatial { return new Geospatial( data.marketDistance ?? 0, data.schoolDistance ?? 0, - data.crimeRate ?? 0, - data.safetyIndex ?? 0, - data.s2StudentRatio ?? 0, data.railwayDistance ?? 0, ); } @@ -602,6 +587,10 @@ export class House { this.images = images; this.geospatial = geospatial; this.value = value; + this.distanceTrain = 0; + this.distanceLightRail = 0; + this.distanceTram = 0; + this.statisticalArea = null; } /** @param {HouseParameter} param */ @@ -704,19 +693,6 @@ export class House { } } -export class Weights { - constructor() { - this.price = 0.5; - this.distanceMarket = 0.5; - this.distanceSchool = 0.5; - this.crimeRate = 0.5; - this.safety = 0.5; - this.s2Students = 0.5; - this.distanceRailway = 0.5; - this.constructionYear = 0.5; - } -} - export class Filters { constructor() { this.minPrice = 0; @@ -793,7 +769,7 @@ export class Collection { DataProvider.getLightRailStops(), DataProvider.getLightRailTracks(), ]); - return new Collection( + const collection = new Collection( districts, houses, trainStations, @@ -806,13 +782,61 @@ export class Collection { lightRailStops, lightRailTracks, ); + collection.#precomputeHouseData(); + return collection; + } + + /** + * Precompute statistical area references and transit distances for all houses + */ + #precomputeHouseData() { + for (const house of this.houses) { + // Find statistical area for this house + house.statisticalArea = this.#findStatisticalArea(house.coordinates); + + // Calculate transit distances + house.distanceTrain = this.#calculateMinDistanceToStations( + house.coordinates, + this.trainStations, + ); + house.distanceLightRail = this.#calculateMinDistanceToStations( + house.coordinates, + this.lightRailStops, + ); + } + } + + /** + * Find statistical area that contains the house coordinates + * @param {Point} coordinates + * @returns {StatisticalArea|null} + */ + #findStatisticalArea(coordinates) { + for (const area of this.statisticalAreas) { + if (area.contains(coordinates)) { + return area; + } + } + return null; } /** + * Calculate minimum distance to any station in the array * @param {Point} point + * @param {TrainStation[]} stations + * @returns {number} distance in meters */ - static getDistanceToRail(point) { - return 0; + #calculateMinDistanceToStations(point, stations) { + if (stations.length === 0) return Infinity; + + let minDistance = Infinity; + for (const station of stations) { + const distance = Point.distance(point, station.polygon.centroid()); + if (distance < minDistance) { + minDistance = distance; + } + } + return minDistance; } } @@ -940,6 +964,27 @@ export class DataProvider { } } +export class Weights { + constructor() { + this.price = 0.5; + this.distanceMarket = 0.5; + this.distanceSchool = 0.5; + // Removed: crimeRate, safety, s2Students + this.distanceRailway = 0.5; + this.constructionYear = 0.5; + this.livingArea = 0.5; + // New weights for statistical area data + this.foreignSpeakers = 0.5; + this.unemploymentRate = 0.5; + this.averageIncome = 0.5; + this.higherEducation = 0.5; + // New weights for transit distances + this.distanceTrain = 0.5; + this.distanceLightRail = 0.5; + this.distanceTram = 0.5; + } +} + export class ScoringEngine { /** * @param {House} house @@ -949,64 +994,108 @@ export class ScoringEngine { static calculate(house, weights) { let score = 0; let totalWeight = 0; - const g = house.geospatial; + // Price scoring (lower is better) if (weights.price > 0) { - const priceScore = Math.max(0, 1 - house.price / 1_000_000); + const priceScore = Math.max(0, 1 - house.price / 2_000_000); // Normalize to 2M score += priceScore * weights.price; totalWeight += weights.price; } + // Construction year scoring (newer is better) if (weights.constructionYear > 0 && house.constructionYear > 0) { const currentYear = new Date().getFullYear(); - const yearScore = (house.constructionYear - 1950) / (currentYear - 1950); + const yearScore = (house.constructionYear - 1900) / (currentYear - 1900); score += yearScore * weights.constructionYear; totalWeight += weights.constructionYear; } - if (weights.constructionYear > 0) { + // Living area scoring (bigger is better) + if (weights.livingArea > 0) { const areaScore = Math.min(1, house.livingArea / 200); - score += areaScore * weights.constructionYear; - totalWeight += weights.constructionYear; + score += areaScore * weights.livingArea; + totalWeight += weights.livingArea; } - if (weights.distanceMarket > 0 && g.marketDistance > 0) { - const marketScore = Math.max(0, 1 - g.marketDistance / 5000); + // Market distance scoring (closer is better) + if (weights.distanceMarket > 0 && house.geospatial.marketDistance > 0) { + const marketScore = Math.max(0, 1 - house.geospatial.marketDistance / 5000); score += marketScore * weights.distanceMarket; totalWeight += weights.distanceMarket; } - if (weights.distanceSchool > 0 && g.schoolDistance > 0) { - const schoolScore = Math.max(0, 1 - g.schoolDistance / 5000); + // School distance scoring (closer is better) + if (weights.distanceSchool > 0 && house.geospatial.schoolDistance > 0) { + const schoolScore = Math.max(0, 1 - house.geospatial.schoolDistance / 5000); score += schoolScore * weights.distanceSchool; totalWeight += weights.distanceSchool; } - if (weights.crimeRate > 0 && g.crimeRate > 0) { - const crimeScore = Math.max(0, 1 - g.crimeRate / 10); - score += crimeScore * weights.crimeRate; - totalWeight += weights.crimeRate; + // Statistical area factors + if (house.statisticalArea) { + const sa = house.statisticalArea; + + // Foreign speakers (lower is generally better) + if (weights.foreignSpeakers > 0) { + const foreignScore = Math.max(0, 1 - sa.foreignSpeakers); + score += foreignScore * weights.foreignSpeakers; + totalWeight += weights.foreignSpeakers; + } + + // Unemployment rate (lower is better) + if (weights.unemploymentRate > 0) { + const unemploymentScore = Math.max(0, 1 - sa.unemploymentRate / 100); + score += unemploymentScore * weights.unemploymentRate; + totalWeight += weights.unemploymentRate; + } + + // Average income (higher is better) + if (weights.averageIncome > 0) { + const incomeScore = Math.min(1, sa.averageIncome / 100000); + score += incomeScore * weights.averageIncome; + totalWeight += weights.averageIncome; + } + + // Higher education (higher is better) + if (weights.higherEducation > 0) { + const educationScore = sa.higherEducation; + score += educationScore * weights.higherEducation; + totalWeight += weights.higherEducation; + } } - if (weights.safety > 0 && g.safetyIndex > 0) { - const safetyScore = g.safetyIndex / 10; - score += safetyScore * weights.safety; - totalWeight += weights.safety; + // Transit distances (closer is better, but not too close) + if (weights.distanceTrain > 0 && house.distanceTrain > 0) { + const trainScore = ScoringEngine.calculateTransitScore(house.distanceTrain); + score += trainScore * weights.distanceTrain; + totalWeight += weights.distanceTrain; } - if (weights.s2Students > 0 && g.s2StudentRatio > 0) { - const studentScore = 1 - Math.abs(g.s2StudentRatio - 0.5); - score += studentScore * weights.s2Students; - totalWeight += weights.s2Students; + if (weights.distanceLightRail > 0 && house.distanceLightRail > 0) { + const lightRailScore = ScoringEngine.calculateTransitScore(house.distanceLightRail); + score += lightRailScore * weights.distanceLightRail; + totalWeight += weights.distanceLightRail; } - if (weights.distanceRailway > 0 && g.railwayDistance > 0) { - const dist = g.railwayDistance; - const railwayScore = dist > 500 ? Math.min(1, (2000 - dist) / 1500) : Math.min(1, dist / 500); - score += railwayScore * weights.distanceRailway; - totalWeight += weights.distanceRailway; + if (weights.distanceTram > 0 && house.distanceTram > 0) { + const tramScore = ScoringEngine.calculateTransitScore(house.distanceTram); + score += tramScore * weights.distanceTram; + totalWeight += weights.distanceTram; } return totalWeight > 0 ? (score / totalWeight) * 100 : 0; } + + /** + * Calculate transit distance score (optimal around 500m-1000m) + * @param {number} distance in meters + * @returns {number} score 0-1 + */ + static calculateTransitScore(distance) { + if (distance < 100) return 0.3; // Too close - noise and congestion + if (distance < 500) return 0.8; // Good walking distance + if (distance < 1000) return 1.0; // Optimal + if (distance < 2000) return 0.6; // Acceptable + return 0.2; // Too far + } } |
