Согласование радиуса: summary = max_distance × средний психотип-множитель
- search_engine: max_distance_km передаётся в analyzer (case_data). - rules_analysis: search_radius = max_distance × _psychotype_radius_multiplier (среднее по полосам 0-500/0.5-1.5/1.5-2.5/2.5+). Убран двойной учёт age/season: score_zone.distance_multiplier их уже содержит, а max_distance их тоже учёл — раньше radius = multiplier×2.0 (хардкод) давал 1.6 км при радиусе 2.4 км. - UI: подсказка у «×0.8» — коэффициент скоринга, не радиуса. - Тест: согласованность summary↔формула.
This commit is contained in:
@@ -70,3 +70,20 @@ class TestZoneReason:
|
|||||||
r2 = _build_zone_reason(dict(zone), dict(case))
|
r2 = _build_zone_reason(dict(zone), dict(case))
|
||||||
assert r1 == r2
|
assert r1 == r2
|
||||||
assert 'совпадает с направлением' in r1
|
assert 'совпадает с направлением' in r1
|
||||||
|
class TestRadiusConsistency:
|
||||||
|
def test_summary_radius_matches_max_times_multiplier(self):
|
||||||
|
"""Радиус в summary = max_distance × психотип-множитель (согласован с хедером)."""
|
||||||
|
import asyncio
|
||||||
|
from services.rules_analysis import rules_analyze
|
||||||
|
case = {
|
||||||
|
'age': 9, 'gender': 'м', 'season': 'лето', 'elapsed_hours': 4.0,
|
||||||
|
'profiles': ['гармоничный'], 'psychotype': 'harmonic',
|
||||||
|
'psychotype_modifiers': {'zone_0_500': 1.0, 'zone_500_1500': 1.0,
|
||||||
|
'zone_1500_2500': 1.1, 'zone_2500plus': 1.0},
|
||||||
|
'max_distance_km': 2.4,
|
||||||
|
'lat': 54.5, 'lon': 28.5,
|
||||||
|
}
|
||||||
|
result = asyncio.run(rules_analyze(case))
|
||||||
|
# Средний психотип-множитель полос (1.0+1.0+1.1+1.0)/4 = 1.025 → 2.4×1.025=2.46→2.5
|
||||||
|
assert result.search_radius_km == 2.5, result.search_radius_km
|
||||||
|
assert f'{result.search_radius_km} км' in result.summary
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ const AnalysisResult = () => {
|
|||||||
<div className="decision-value radius">
|
<div className="decision-value radius">
|
||||||
{analysisData?.max_distance_km} км
|
{analysisData?.max_distance_km} км
|
||||||
{analysisData?.distance_multiplier !== 1 && (
|
{analysisData?.distance_multiplier !== 1 && (
|
||||||
<span className="radius-mult">×{analysisData?.distance_multiplier}</span>
|
<span
|
||||||
|
className="radius-mult"
|
||||||
|
title="Суммарный коэффициент скоринга (возраст/сезон/профили) — влияет на сравнение секторов, не на радиус."
|
||||||
|
>×{analysisData?.distance_multiplier}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,6 +178,17 @@ def _psychotype_phrase(case_data: dict, distance_km: float) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _psychotype_radius_multiplier(case_data: dict) -> float:
|
||||||
|
"""Средний множитель психотипа по полосам дистанции (без двойного учёта
|
||||||
|
возраст/сезон — они уже в max_distance)."""
|
||||||
|
mods = case_data.get('psychotype_modifiers')
|
||||||
|
if not mods:
|
||||||
|
return 1.0
|
||||||
|
vals = [mods.get(k, 1.0) for k in ('zone_0_500', 'zone_500_1500',
|
||||||
|
'zone_1500_2500', 'zone_2500plus')]
|
||||||
|
return sum(vals) / len(vals)
|
||||||
|
|
||||||
|
|
||||||
def _build_zone_reason(zone: dict, case_data: dict) -> str:
|
def _build_zone_reason(zone: dict, case_data: dict) -> str:
|
||||||
"""Словесное пояснение приоритета зоны — по тем же факторам, что считает
|
"""Словесное пояснение приоритета зоны — по тем же факторам, что считает
|
||||||
WeightedScorer (детерминированно, без выдумок): рельеф, вода, дороги, НП,
|
WeightedScorer (детерминированно, без выдумок): рельеф, вода, дороги, НП,
|
||||||
@@ -238,7 +249,8 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult:
|
|||||||
urgency = _age_urgency(age, profiles)
|
urgency = _age_urgency(age, profiles)
|
||||||
|
|
||||||
primary_zones: list[PrimaryZone] = []
|
primary_zones: list[PrimaryZone] = []
|
||||||
search_radius_km = 5.0
|
base_km = float(case_data.get('max_distance_km') or 5.0)
|
||||||
|
search_radius_km = round(base_km * 1.0, 1)
|
||||||
|
|
||||||
if case_data.get('lat') and case_data.get('lon'):
|
if case_data.get('lat') and case_data.get('lon'):
|
||||||
try:
|
try:
|
||||||
@@ -273,7 +285,11 @@ async def rules_analyze(case_data: dict[str, Any]) -> AnalysisResult:
|
|||||||
distance=zone['distance_km'],
|
distance=zone['distance_km'],
|
||||||
reason=_build_zone_reason(zone, case_data),
|
reason=_build_zone_reason(zone, case_data),
|
||||||
))
|
))
|
||||||
search_radius_km = scorer.distance_multiplier * 2.0
|
# Радиус = max_distance (НормС×коэффициенты) × средний психотип-множитель.
|
||||||
|
# score_zone.distance_multiplier НЕ используется: он накопил age/season,
|
||||||
|
# уже учтённые в max_distance — двойной учёт.
|
||||||
|
base_km = float(case_data.get('max_distance_km') or 2.0)
|
||||||
|
search_radius_km = round(base_km * _psychotype_radius_multiplier(case_data), 1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f'Geo/scoring service error: {e}')
|
logger.warning(f'Geo/scoring service error: {e}')
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ async def build_search_model(
|
|||||||
case_data['psychotype_modifiers'] = psychotype_modifiers
|
case_data['psychotype_modifiers'] = psychotype_modifiers
|
||||||
|
|
||||||
max_distance_km = calculate_max_distance(case_data)
|
max_distance_km = calculate_max_distance(case_data)
|
||||||
|
case_data['max_distance_km'] = max_distance_km
|
||||||
analysis = await analyzer(case_data)
|
analysis = await analyzer(case_data)
|
||||||
|
|
||||||
scorer = WeightedScorer()
|
scorer = WeightedScorer()
|
||||||
|
|||||||
Reference in New Issue
Block a user