From a6e95b65c258ac9a0318e799f42039dab98c19d4 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Tue, 12 May 2026 07:56:03 +0300 Subject: [PATCH] fix(site-finder): X1 logic bugs from PR #87 re-review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/app/api/v1/parcels.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index 9f8e7029..7743f5cb 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -1,3 +1,4 @@ +import datetime as _dt import json import logging import math @@ -579,8 +580,6 @@ def _compute_confidence( доступны на main. Композитный балл = avg of subscore'ов; caveats — list конкретных проблем для UI ("Нет данных N, score K ненадёжен"). """ - import datetime as _dt - caveats: list[str] = [] subscores: dict[str, float] = {} @@ -616,9 +615,15 @@ def _compute_confidence( if not district_row: caveats.append("Район не определён (вне границ ЕКБ?) — медианные цены недоступны") - # 4) Market trend — есть ли rosreestr_deals - if market_trend and market_trend.get("recent_deals_count"): - n_recent = int(market_trend["recent_deals_count"]) + # 4) Market trend — есть ли rosreestr_deals. + # Guard `int(... or 0)` — recent_deals_count иногда приходит как non-numeric + # из external/legacy paths; без guard int() крашнет 500. + n_recent_raw = (market_trend or {}).get("recent_deals_count") + try: + n_recent = int(n_recent_raw) if n_recent_raw is not None else 0 + except (ValueError, TypeError): + n_recent = 0 + if n_recent > 0: # порог 5 сделок за 6 мес — достаточно для тренда subscores["market_trend"] = min(1.0, n_recent / 10.0) if n_recent < 5: @@ -1300,11 +1305,14 @@ def analyze_parcel( factors_sorted = sorted(factors_detailed, key=lambda x: x["contribution"], reverse=True) # Convention: оба top-list'а отсортированы "dominant first": - # positives → most-positive first (просто [:3] от descending sort) - # negatives → most-negative first ([-3:][::-1] = последние три (наиболее - # отрицательные) реверсом, чтобы first item был самым большим минусом) + # positives → most-positive first (factors_sorted desc → [:3]) + # negatives → most-negative first (sort negatives asc → [:3]) + # Раньше использовался trick [-3:][::-1] на desc-sorted — это давало тот же + # результат для N>=3 negatives, но был неинтуитивно читать; explicit sort asc + # короче и не зависит от тонкостей slicing. score_top_3_positives = [f for f in factors_sorted if f["contribution"] > 0][:3] - score_top_3_negatives = [f for f in factors_sorted if f["contribution"] < 0][-3:][::-1] + negatives_only = [f for f in factors_sorted if f["contribution"] < 0] + score_top_3_negatives = sorted(negatives_only, key=lambda x: x["contribution"])[:3] # By-group totals — для stacked-bar в UI. count это int, contribution* — float. group_totals: dict[str, dict[str, float | int]] = {}