diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cecc5a..1bae5b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed +- **Market Score v3 (Marktreife-Score recalibration)** — fixes ranking inversion where early-stage markets (Germany 1/100k) outscored mature markets (Spain 36/100k): + - **Formula rewrite** (`city_market_profile.sql`): supply development now 40 pts (log-scaled density LN(d+1)/LN(21) × count gate min(1,count/5)); demand evidence 25 pts (occupancy or 40% density proxy); population reduced to 15 pts (context); income to 10 pts (context); data quality to 10 pts; saturation discount removed + - **Count gate** eliminates small-town inflation: a single venue in a 5k-resident town can no longer outscore Berlin (was 92.7 → now 43.9 for Bernau bei Berlin) + - **LN ceiling at 20/100k** (was linear 4/100k) gives meaningful differentiation from 0 to 20: Málaga 70.1, Barcelona 67.4, Madrid 66.9, Amsterdam 58.4, Berlin 42.2, London 44.1 + - **Template thresholds updated** across all 3 pSEO templates (city-cost-de, country-overview, city-pricing): color coding green ≥55 (was ≥65) / amber ≥35 (was ≥40); intro/FAQ tiers strong ≥55 (was ≥70) / mid ≥35 (was ≥45); white-space signal interplay market_score < 40 (was < 50) + +- **Opportunity Score supply gap ceiling raised 4→8/100k** (`location_opportunity_profile.sql`) — gentler gradient for partially-served markets; accounts for ~87% data undercount vs FIP real-world totals. Documents discovered formula behaviour: DuckDB `LEAST(1.0, NULL)=1.0` means NULL catchment already yields full 15 pts; income PPS saturates for all EU countries; tennis courts data currently empty (formula correct, data pending) + ### Added - **Opportunity Score integration** — second scoring dimension (`Marktpotenzial`) now visible in city and country articles: - **SQL chain**: `dim_cities` now carries `geoname_id` (from the existing GeoNames LEFT JOIN); threaded through `city_market_profile` → `pseo_city_costs_de` which LEFT JOINs `location_opportunity_profile` on `(country_code, geoname_id)`; `pseo_country_overview` gains `avg_opportunity_score`, `top_opportunity_score`, `top_opportunity_slugs`, `top_opportunity_names` diff --git a/PROJECT.md b/PROJECT.md index 4f21dc4..3dc0649 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1,7 +1,7 @@ # Padelnomics — Project Tracker > Move tasks across columns as you work. Add new tasks at the top of the relevant column. -> Last updated: 2026-02-25. +> Last updated: 2026-02-27. --- @@ -87,6 +87,8 @@ - [x] Markets hub (`//markets`) — article listing with FTS + country/region filters - [x] DuckDB refresh script (`refresh_from_daas.py`) - [x] **Opportunity Score integration** — `opportunity_score` (Marktpotenzial) wired into city + country templates; `geoname_id` threaded through SQL chain (dim_cities → city_market_profile → pseo_city_costs_de); 71.4% city match rate; stats strip, intro paragraphs, market tables, and FAQ updated in both DE + EN +- [x] **Market Score v3 recalibration** — fixes ranking inversion (Germany 1/100k was outscoring Spain 36/100k); log-scaled density + count gate replaces linear formula; saturation discount removed; template thresholds updated across all 3 pSEO templates; verified: Málaga 70.1, Barcelona 67.4, Madrid 66.9, Amsterdam 58.4, Bernau 43.9 (was 92.7), Berlin 42.2, London 44.1 +- [x] **Opportunity Score v2** — supply gap ceiling raised 4→8/100k (gentler gradient, accounts for 87% data undercount); formula documentation added (DuckDB LEAST NULL behaviour, income saturation, tennis data gap) ### Data Pipeline (DaaS) - [x] Overpass API extractor (OSM padel courts)