feat(site-finder): X1 score breakdown + verbal explain (#47) #87

Closed
lekss361 wants to merge 8 commits from feat/site-finder-score-breakdown into main

8 commits

Author SHA1 Message Date
lekss361
a6e95b65c2 fix(site-finder): X1 logic bugs from PR #87 re-review
1. score_top_3_negatives — rewrite to unambiguously "most-negative first" via
   explicit `sorted(negatives, key=contribution)[:3]` (ascending sort). Раньше
   использовался trick `[-3:][::-1]` на desc-sorted — давал тот же результат
   для N>=3 negatives, но неинтуитивно читать.
2. _compute_confidence(recent_deals_count) — guard `int(... or 0)` через
   try/except (ValueError, TypeError). Защищает от non-numeric строки в
   external/legacy market_trend payload.
3. Style: `import datetime as _dt` перенесён из function-scope в module-level.

Per auto-review on d0fa8a3.
2026-05-12 07:56:03 +03:00
lekss361
d0fa8a3126 merge: incorporate remote feat/site-finder-score-breakdown (rebased) 2026-05-12 07:44:19 +03:00
lekss361
ec681441e5 fix(site-finder): unique factor key (X1 #47) — prevent React collision
PR #87 auto-review нашёл: два POI одной категории на одинаковом округлённом
расстоянии (например two аптеки 450м в плотном районе центра ЕКБ) давали
duplicate factor key "pharmacy_450m" → silent React reconciliation errors.

Fix: добавил enumerate index в slug: f"{cat}_{round(distance_m)}m_{idx}".
1 строка, backward compat (factor — opaque slug, не парсится клиентом).

Per auto-review on 97dc4ba.
2026-05-12 07:39:27 +03:00
lekss361
b5a218160d fix(site-finder): address PR #87 auto-review minor feedback
Backend (parcels.py):
- Centrality factor: weight=1.0 вместо weight=center_bonus (semantic — decay
  для bonus не применяется, contribution = bonus IS the value). Защищает
  future PDF-export/UI который мог бы показать weight отдельно от contribution.
- group_totals type: dict[str, dict[str, float | int]] — count это int,
  contribution и contribution_pct это float. Уточняет hint для future mypy.
- Top-3 sort convention — добавлен inline-комментарий: positives "[:3] от
  descending" (most-positive first); negatives "[-3:][::-1]" (most-negative
  first). Оба "dominant first".

Frontend (ScoreBreakdownPanel.tsx):
- Stacked bar legend orphan fix: positive groups идут в legend под баром (как
  до того); negative groups показываются отдельной строкой ниже "Снижают балл —
  Шум/трамвай: −0.46". Никаких swatch'ей без bar-сегмента.

Per auto-review on 1d1c169.
2026-05-12 07:39:27 +03:00
lekss361
1002214f12 feat(site-finder): X1 score breakdown + verbal explain (#47)
Backend:
- POST /parcels/{cad}/analyze returns score_breakdown_detailed (per-POI
  FactorContribution with verbal explain), score_top_3_positives /
  score_top_3_negatives, score_by_group (для stacked bar).
- Каждый фактор: factor slug, category, category_ru, group, value, weight,
  contribution, contribution_pct, verbal.
- center_bonus добавлен как synthetic factor группы "Локация".
- Skip факторов с |contribution| < 0.01 (POI > 1км) — UI шуму не нужен.

Frontend:
- Новый ScoreBreakdownPanel.tsx: stacked-bar по группам с легендой,
  топ-3 плюса / топ-3 минуса с verbal, разворачиваемая таблица всех факторов.
- Интегрирован в OverviewTab под секцией "Балл".
- TS типы расширены: FactorContribution, ScoreGroupTotal.

Closes #47.
2026-05-12 07:39:27 +03:00
lekss361
c46637ab61 fix(site-finder): unique factor key (X1 #47) — prevent React collision
PR #87 auto-review нашёл: два POI одной категории на одинаковом округлённом
расстоянии (например two аптеки 450м в плотном районе центра ЕКБ) давали
duplicate factor key "pharmacy_450m" → silent React reconciliation errors.

Fix: добавил enumerate index в slug: f"{cat}_{round(distance_m)}m_{idx}".
1 строка, backward compat (factor — opaque slug, не парсится клиентом).

Per auto-review on 97dc4ba.
2026-05-12 01:14:29 +03:00
lekss361
97dc4ba081 fix(site-finder): address PR #87 auto-review minor feedback
Backend (parcels.py):
- Centrality factor: weight=1.0 вместо weight=center_bonus (semantic — decay
  для bonus не применяется, contribution = bonus IS the value). Защищает
  future PDF-export/UI который мог бы показать weight отдельно от contribution.
- group_totals type: dict[str, dict[str, float | int]] — count это int,
  contribution и contribution_pct это float. Уточняет hint для future mypy.
- Top-3 sort convention — добавлен inline-комментарий: positives "[:3] от
  descending" (most-positive first); negatives "[-3:][::-1]" (most-negative
  first). Оба "dominant first".

Frontend (ScoreBreakdownPanel.tsx):
- Stacked bar legend orphan fix: positive groups идут в legend под баром (как
  до того); negative groups показываются отдельной строкой ниже "Снижают балл —
  Шум/трамвай: −0.46". Никаких swatch'ей без bar-сегмента.

Per auto-review on 1d1c169.
2026-05-12 00:54:32 +03:00
lekss361
1d1c169365 feat(site-finder): X1 score breakdown + verbal explain (#47)
Backend:
- POST /parcels/{cad}/analyze returns score_breakdown_detailed (per-POI
  FactorContribution with verbal explain), score_top_3_positives /
  score_top_3_negatives, score_by_group (для stacked bar).
- Каждый фактор: factor slug, category, category_ru, group, value, weight,
  contribution, contribution_pct, verbal.
- center_bonus добавлен как synthetic factor группы "Локация".
- Skip факторов с |contribution| < 0.01 (POI > 1км) — UI шуму не нужен.

Frontend:
- Новый ScoreBreakdownPanel.tsx: stacked-bar по группам с легендой,
  топ-3 плюса / топ-3 минуса с verbal, разворачиваемая таблица всех факторов.
- Интегрирован в OverviewTab под секцией "Балл".
- TS типы расширены: FactorContribution, ScoreGroupTotal.

Closes #47.
2026-05-12 00:43:34 +03:00