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.
This commit is contained in:
parent
1d1c169365
commit
97dc4ba081
2 changed files with 45 additions and 5 deletions
|
|
@ -612,7 +612,9 @@ def analyze_parcel(
|
||||||
else:
|
else:
|
||||||
center_bonus = 0.0
|
center_bonus = 0.0
|
||||||
|
|
||||||
# X1 (#47): centrality как отдельный synthetic factor в breakdown
|
# X1 (#47): centrality как отдельный synthetic factor в breakdown.
|
||||||
|
# NB: для centrality decay не применяется (bonus IS the value), поэтому
|
||||||
|
# weight=1.0 семантически — "no decay multiplier"; contribution = center_bonus.
|
||||||
if center_bonus > 0:
|
if center_bonus > 0:
|
||||||
factors_detailed.append(
|
factors_detailed.append(
|
||||||
{
|
{
|
||||||
|
|
@ -621,7 +623,7 @@ def analyze_parcel(
|
||||||
"category_ru": "Центральность",
|
"category_ru": "Центральность",
|
||||||
"group": "Локация",
|
"group": "Локация",
|
||||||
"value": round(dist_to_center_km, 2),
|
"value": round(dist_to_center_km, 2),
|
||||||
"weight": center_bonus,
|
"weight": 1.0,
|
||||||
"contribution": round(center_bonus, 2),
|
"contribution": round(center_bonus, 2),
|
||||||
"verbal": (
|
"verbal": (
|
||||||
f"Близость к центру ЕКБ ({dist_to_center_km:.1f}км) — "
|
f"Близость к центру ЕКБ ({dist_to_center_km:.1f}км) — "
|
||||||
|
|
@ -1004,11 +1006,15 @@ def analyze_parcel(
|
||||||
f["contribution_pct"] = round(100.0 * abs(f["contribution"]) / abs_total, 1)
|
f["contribution_pct"] = round(100.0 * abs(f["contribution"]) / abs_total, 1)
|
||||||
|
|
||||||
factors_sorted = sorted(factors_detailed, key=lambda x: x["contribution"], reverse=True)
|
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 был самым большим минусом)
|
||||||
score_top_3_positives = [f for f in factors_sorted if f["contribution"] > 0][:3]
|
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]
|
score_top_3_negatives = [f for f in factors_sorted if f["contribution"] < 0][-3:][::-1]
|
||||||
|
|
||||||
# By-group totals — для stacked-bar в UI
|
# By-group totals — для stacked-bar в UI. count это int, contribution* — float.
|
||||||
group_totals: dict[str, dict[str, float]] = {}
|
group_totals: dict[str, dict[str, float | int]] = {}
|
||||||
for f in factors_detailed:
|
for f in factors_detailed:
|
||||||
g = group_totals.setdefault(
|
g = group_totals.setdefault(
|
||||||
f["group"], {"contribution": 0.0, "count": 0, "contribution_pct": 0.0}
|
f["group"], {"contribution": 0.0, "count": 0, "contribution_pct": 0.0}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,8 @@ export function ScoreBreakdownPanel({
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{byGroup.map((g) => (
|
{/* Positive groups — visible в баре */}
|
||||||
|
{positiveGroups.map((g) => (
|
||||||
<div
|
<div
|
||||||
key={g.group}
|
key={g.group}
|
||||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
|
|
@ -134,6 +135,39 @@ export function ScoreBreakdownPanel({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Negative groups — отдельной "drag" линией под баром (legend для bar
|
||||||
|
использует только positive, чтобы не было orphan swatches без сегмента) */}
|
||||||
|
{byGroup
|
||||||
|
.filter((g) => g.contribution < 0)
|
||||||
|
.map((g) => (
|
||||||
|
<div
|
||||||
|
key={`neg-${g.group}`}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
marginTop: 6,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6b7280",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: GROUP_COLORS[g.group] ?? GROUP_COLORS.Прочее,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Снижают балл — {g.group}:{" "}
|
||||||
|
<strong style={{ color: "#dc2626" }}>
|
||||||
|
{fmtContribution(g.contribution)}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue