merge: Opportunity Score v8 — better spread/discrimination
All checks were successful
CI / test (push) Successful in 54s
CI / tag (push) Successful in 3s

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Deeman
2026-03-09 22:24:43 +01:00
4 changed files with 40 additions and 32 deletions

View File

@@ -19,20 +19,23 @@
-- 10 pts economic context — income PPS normalised to 25,000 ceiling
-- 10 pts data quality — completeness discount
--
-- Padelnomics Opportunity Score (Marktpotenzial-Score v7, 0100):
-- Padelnomics Opportunity Score (Marktpotenzial-Score v8, 0100):
-- "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).
--
-- v7 changes: country-level supply saturation dampener on supply deficit.
-- Saturated countries (Spain 7.4/100k) get dampened supply deficit (×0.30 → 12 pts max).
-- Emerging markets (Germany 0.24/100k) are nearly unaffected (×0.98 → ~39 pts).
-- Floor at 0.3 so supply deficit never fully vanishes.
-- v8 changes: better spread/discrimination.
-- - Reweight: addressable market 20→15, economic power 15→10, supply deficit 40→50.
-- - Supply deficit existence dampener: country_venues/50 factor (0.11.0).
-- Zero-venue countries get max 5 pts supply deficit (was 50).
-- - Steeper addressable market curve: LN/500K → SQRT/1M.
-- - NULL distance gap → 0.0 (was 0.5). Unknown = assume nearby.
-- - Added country_percentile output column (PERCENT_RANK within country).
--
-- 20 pts addressable market — log-scaled catchment population, ceiling 500K
-- 15 pts economic power — income PPS, normalised to 35,000
-- 40 pts supply deficit — max(density gap, distance gap) × country dampener
-- 15 pts addressable market — sqrt-scaled catchment population, ceiling 1M
-- 10 pts economic power — income PPS, normalised to 35,000
-- 50 pts supply deficit — max(density gap, distance gap) × existence dampener
-- 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 headroom — inverse country-level avg market maturity
@@ -228,28 +231,29 @@ country_supply AS (
-- Step 4: add opportunity_score using country market validation + supply saturation.
scored AS (
SELECT ms.*,
-- ── Opportunity Score (Marktpotenzial-Score v7, H3 catchment) ──────────
-- ── Opportunity Score (Marktpotenzial-Score v8, H3 catchment) ──────────
ROUND(
-- 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 (40 pts): max of density gap and distance gap.
-- Dampened by country-level supply saturation:
-- Spain (7.4/100k) → dampener 0.3012 pts max
-- Germany (0.24/100k) → dampener 0.98 → ~39 pts max
+ 40.0 * GREATEST(
-- Addressable market (15 pts): sqrt-scaled catchment population, ceiling 1M
15.0 * LEAST(1.0, SQRT(GREATEST(catchment_population, 1) / 1000000.0))
-- Economic power (10 pts): income PPS normalised to 35,000
+ 10.0 * LEAST(1.0, COALESCE(median_income_pps, 15000) / 35000.0)
-- Supply deficit (50 pts): max of density gap and distance gap.
-- Dampened by market existence: country_venues/50 (0.11.0).
-- 0 venues in country → factor 0.1max 5 pts supply deficit
-- 10 venues → 0.2 → max 10 pts
-- 50+ venues → 1.0 → full credit
+ 50.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) / 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)
-- distance-based gap: 30km+ = 1.0, 0km = 0.0; NULL = 0.0 (assume nearby)
COALESCE(LEAST(1.0, nearest_padel_court_km / 30.0), 0.0)
)
-- Country supply dampener: floor 0.3 so deficit never fully vanishes
* GREATEST(0.3, 1.0 - COALESCE(cs.venues_per_100k, 0.0) / 10.0)
-- Market existence dampener: zero-venue countries get 0.1, 50+ venues = 1.0
* GREATEST(0.1, LEAST(1.0, COALESCE(cs.country_venues, 0) / 50.0))
-- Sports culture (10 pts): tennis density as racquet-sport adoption proxy.
-- Ceiling 50 courts within 25km. Harmless when tennis data is zero (contributes 0).
+ 10.0 * LEAST(1.0, COALESCE(tennis_courts_within_25km, 0) / 50.0)
@@ -301,6 +305,9 @@ SELECT
END AS catchment_venues_per_100k,
LEAST(GREATEST(s.market_score, 0), 100) AS market_score,
LEAST(GREATEST(s.opportunity_score, 0), 100) AS opportunity_score,
ROUND(PERCENT_RANK() OVER (
PARTITION BY s.country_code ORDER BY s.opportunity_score
) * 100, 0) AS country_percentile,
s.median_hourly_rate,
s.median_peak_rate,
s.median_offpeak_rate,