aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-13 18:55:58 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-13 18:55:58 +0200
commite92bf0544d423f0ab2ff7801f02b17f863a97387 (patch)
treedace83622f146672aa4f2902d4935523e75847e6
parente4fdd8457d2d320eea502f0801fc22eceb8947b1 (diff)
downloadhousing-e92bf0544d423f0ab2ff7801f02b17f863a97387.tar.zst
Update score calculation
-rw-r--r--app/components.js83
-rw-r--r--app/models.js209
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
+ }
}