diff --git a/transform/sqlmesh_padelnomics/models/serving/location_profiles.sql b/transform/sqlmesh_padelnomics/models/serving/location_profiles.sql index 5d5f36e..9e5483b 100644 --- a/transform/sqlmesh_padelnomics/models/serving/location_profiles.sql +++ b/transform/sqlmesh_padelnomics/models/serving/location_profiles.sql @@ -19,22 +19,22 @@ -- 10 pts economic context — income PPS normalised to 25,000 ceiling -- 10 pts data quality — completeness discount -- --- Padelnomics Opportunity Score (Marktpotenzial-Score v5, 0–100): +-- Padelnomics Opportunity Score (Marktpotenzial-Score v6, 0–100): -- "Where should I build a padel court?" -- Computed for ALL locations — zero-court locations score highest on supply deficit. -- H3 catchment methodology: addressable market and supply deficit use a regional -- H3 catchment (res-5 cell + 6 neighbours, ~24km radius). -- --- v5 changes: merge supply gap + catchment gap → single supply deficit (35 pts), --- add sports culture proxy (10 pts, tennis density), add construction affordability (5 pts), --- reduce economic power from 20 → 15 pts. +-- v6 changes: lower density ceiling 8→5/100k (saturated markets hit zero-gap sooner), +-- increase supply deficit weight 35→40 pts, reduce addressable market 25→20 pts, +-- invert market validation (high country maturity = LESS opportunity). -- --- 25 pts addressable market — log-scaled catchment population, ceiling 500K +-- 20 pts addressable market — log-scaled catchment population, ceiling 500K -- 15 pts economic power — income PPS, normalised to 35,000 --- 35 pts supply deficit — max(density gap, distance gap); eliminates double-count +-- 40 pts supply deficit — max(density gap, distance gap); eliminates double-count -- 10 pts sports culture — tennis court density as racquet-sport adoption proxy -- 5 pts construction affordability — income relative to construction costs (PLI) --- 10 pts market validation — country-level avg market maturity (from market_scored CTE) +-- 10 pts market headroom — inverse country-level avg market maturity -- -- Consumers query directly with WHERE filters: -- cities API: WHERE country_slug = ? AND city_slug IS NOT NULL @@ -198,9 +198,9 @@ market_scored AS ( END AS market_score FROM with_pricing ), --- Step 2: country-level avg market maturity — used as market validation signal (10 pts). +-- Step 2: country-level avg market maturity — used as market headroom signal (10 pts). -- Filter to market_score > 0 (cities with padel courts only) so zero-court locations --- don't dilute the country signal. ES proven demand → ~60, SE struggling → ~35. +-- don't dilute the country signal. Higher avg = more saturated = less headroom. country_market AS ( SELECT country_code, @@ -212,21 +212,21 @@ country_market AS ( -- Step 3: add opportunity_score using country market validation signal. scored AS ( SELECT ms.*, - -- ── Opportunity Score (Marktpotenzial-Score v5, H3 catchment) ────────── + -- ── Opportunity Score (Marktpotenzial-Score v6, H3 catchment) ────────── ROUND( - -- Addressable market (25 pts): log-scaled catchment population, ceiling 500K - 25.0 * LEAST(1.0, LN(GREATEST(catchment_population, 1)) / LN(500000)) + -- Addressable market (20 pts): log-scaled catchment population, ceiling 500K + 20.0 * LEAST(1.0, LN(GREATEST(catchment_population, 1)) / LN(500000)) -- Economic power (15 pts): income PPS normalised to 35,000 + 15.0 * LEAST(1.0, COALESCE(median_income_pps, 15000) / 35000.0) - -- Supply deficit (35 pts): max of density gap and distance gap. - -- Merges old supply gap (30) + catchment gap (15) which were ~80% correlated. - + 35.0 * GREATEST( - -- density-based gap (H3 catchment): 0 courts = 1.0, 8/100k = 0.0 + -- Supply deficit (40 pts): max of density gap and distance gap. + -- Ceiling 5/100k (down from 8): Spain at 6-16/100k now hits zero-gap. + + 40.0 * GREATEST( + -- density-based gap (H3 catchment): 0 courts = 1.0, 5/100k = 0.0 GREATEST(0.0, 1.0 - COALESCE( CASE WHEN catchment_population > 0 THEN GREATEST(catchment_padel_courts, COALESCE(city_padel_venue_count, 0))::DOUBLE / catchment_population * 100000 ELSE 0.0 - END, 0.0) / 8.0), + END, 0.0) / 5.0), -- distance-based gap: 30km+ = 1.0, 0km = 0.0; NULL = 0.5 COALESCE(LEAST(1.0, nearest_padel_court_km / 30.0), 0.5) ) @@ -239,10 +239,11 @@ scored AS ( COALESCE(median_income_pps, 15000) / 35000.0 / GREATEST(0.5, COALESCE(pli_construction, 100.0) / 100.0) ) - -- Market validation (10 pts): country-level avg market maturity. - -- ES (~70/100): proven demand → ~7 pts. SE (~35/100): emerging → ~3.5 pts. - -- NULL (no courts in country yet): 0.5 neutral → 5 pts (untested, not penalised). - + 10.0 * COALESCE(cm.country_avg_market_score / 100.0, 0.5) + -- Market headroom (10 pts): INVERSE country-level avg market maturity. + -- High avg market score = saturated market = LESS opportunity for new entrants. + -- ES (~46/100): proven demand, less headroom → ~5.4 pts. + -- SE (~40/100): emerging → ~6 pts. NULL: 0.5 neutral → 5 pts. + + 10.0 * (1.0 - COALESCE(cm.country_avg_market_score / 100.0, 0.5)) , 1) AS opportunity_score FROM market_scored ms LEFT JOIN country_market cm ON ms.country_code = cm.country_code