From 59f97c850e34b1f90d5cbf07bba8ec48657296bd Mon Sep 17 00:00:00 2001 From: viktot14-ai Date: Wed, 9 Sep 2026 19:33:53 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20forest=5Fpct=20=E2=80=94=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=200..1=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20(?= =?UTF-8?q?=D0=B2=D0=B5=D1=81=20=D0=BB=D0=B5=D1=81=D0=B0=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=B5=D1=82=20=D1=81=D0=BE=20100=C3=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закономерность «приоритетная зона вверх-вправо»: score_zone умножает weight × forest_pct, ожидая ДОЛЮ (тесты скоринга 0.3–0.8), а гео-сервисы (osm_local и legacy Overpass calculate_forest_coverage) отдавали ПРОЦЕНТЫ 0..100. Лес получил фактический вес в 100 раз больше задумки — топ зон систематически занимали чуть более зелёные сектора. Фикс: osm_local.calculate_forest_coverage возвращают долю 0..1. Разброс скорингов по Минску: 1.244 → 0.032 (честно однородный город). Остаточные различия — вода/дороги; дифференциация плотностей город/село — ревью скоринга (B1), коэффициенты не тронуты. +регресс-тест 0<=forest_pct<=1. 197 passed. --- backend/tests/test_osm_local.py | 4 +++- services/geo_service.py | 7 +++++-- services/osm_local.py | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/tests/test_osm_local.py b/backend/tests/test_osm_local.py index 1557f30..6181aad 100644 --- a/backend/tests/test_osm_local.py +++ b/backend/tests/test_osm_local.py @@ -45,7 +45,9 @@ class TestOsmLocal: assert set(f) == {'roads_km', 'road_density', 'water_distance_km', 'settlement_distance_km', 'forest_pct'} assert f['roads_km'] >= 0 - assert 0 <= f['forest_pct'] <= 100 + # Контракт: лес — ДОЛЯ 0..1 (scoring_service умножает weight × forest_pct). + # Регресс: раньше отдавались проценты 0..100 — лес доминировал со 100× весом. + assert 0 <= f['forest_pct'] <= 1 assert f['water_distance_km'] is None or f['water_distance_km'] > 0 def test_zones_differ_between_directions(self): diff --git a/services/geo_service.py b/services/geo_service.py index 431fa4a..816678c 100644 --- a/services/geo_service.py +++ b/services/geo_service.py @@ -344,9 +344,12 @@ def calculate_forest_coverage(elements: List[Dict], radius_m: int) -> float: search_area_km2 = math.pi * (radius_m / 1000) ** 2 estimated_forest_km2 = forest_ways * 0.1 - coverage_pct = min(100.0, (estimated_forest_km2 / search_area_km2) * 100) + # Контракт: ДОЛЯ 0..1 (scoring_service читает forest_pct как долю — + # weight × forest_pct; раньше отдавались проценты 0..100, что делало + # лес доминирующим фактором со 100× весом). + coverage = min(1.0, estimated_forest_km2 / search_area_km2) - return round(coverage_pct, 1) + return round(coverage, 3) async def get_zone_features(lat: float, lon: float, direction: str, radius_m: int) -> Dict: diff --git a/services/osm_local.py b/services/osm_local.py index e3bcaea..e6a90c9 100644 --- a/services/osm_local.py +++ b/services/osm_local.py @@ -145,9 +145,13 @@ def get_zone_features_postgis(lat: float, lon: float, direction: str, radius_m: search_area_km2 = math.pi * (radius_m / 1000.0) ** 2 road_density = roads_km / search_area_km2 if search_area_km2 > 0 else 0.0 + # Контракт Zone.forest_pct — ДОЛЯ леса 0..1 (так его читает + # scoring_service.score_zone: weight × forest_pct; тесты скоринга + # используют 0.3–0.8 как доли). Раньше отдались проценты 0..100 — + # лес в 100 раз перевешивал остальные факторы (зоны «вверх-вправо»). forest_pct = 0.0 if forest_m2 > 0 and search_area_km2 > 0: - forest_pct = round(min(100.0, (forest_m2 / 1e6) / search_area_km2 * 100), 1) + forest_pct = round(min(1.0, (forest_m2 / 1e6) / search_area_km2), 3) water_candidates = [float(m) for m in (water_poly_m, waterway_m) if m is not None] water_distance_km = round(min(water_candidates), 3) if water_candidates else None