Merge pull request 'fix(site-finder): honest POI-score label + drop duplicate MOI column (audit #1871 P2)' (#1877) from audit/1871-p2-invest-moi into main
This commit is contained in:
commit
bfa95e4aee
7 changed files with 43 additions and 25 deletions
|
|
@ -2966,8 +2966,9 @@ def analyze_parcel(
|
|||
"score_label": _score_label(score_final),
|
||||
"score_max_reference": SCORE_MAX_REFERENCE,
|
||||
"score_explanation": (
|
||||
"Сумма close-distance POI (школы/сады/парки +, трамваи -) + center_bonus. "
|
||||
">40 = редко, типичный город. центр 15-30."
|
||||
"POI-скор (0–40, оценочный) — взвешенная сумма близких POI с учётом "
|
||||
"расстояния + бонус за центр. Не инвест-рейтинг: школы/сады/парки "
|
||||
"повышают, трамвайные пути снижают. Типичный город 15–30, >40 редко."
|
||||
),
|
||||
"score_breakdown": by_category,
|
||||
# X1 (#47): per-factor контрибуции с verbal explain + top-3 / by-group.
|
||||
|
|
|
|||
|
|
@ -245,8 +245,8 @@ function SiteFinderContent() {
|
|||
? `${data.score.toFixed(2)} / ${scoreMax.toFixed(0)}`
|
||||
: "—";
|
||||
const scoreLabel = data?.score_label
|
||||
? `Score · ${data.score_label}`
|
||||
: "Score";
|
||||
? `POI-скор · ${data.score_label} · оценочный`
|
||||
: "POI-скор · оценочный";
|
||||
const scoreCardColor: KpiColor = data
|
||||
? scoreKpiColor(data.score, scoreMax)
|
||||
: "neutral";
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ const COMPARISON_ROWS: ComparisonRow[] = [
|
|||
gendesign: "DOM.РФ + Objective, еженедельно",
|
||||
},
|
||||
{
|
||||
feature: "Инвест-балл участка и объяснение",
|
||||
feature: "POI-скор участка и объяснение",
|
||||
nspd: null,
|
||||
gendesign: "Score 0–100 с разбором факторов",
|
||||
gendesign: "POI-скор 0–40 с разбором факторов",
|
||||
},
|
||||
{
|
||||
feature: "Прогноз цены и горизонты выхода",
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ interface Props {
|
|||
value: string;
|
||||
label: string;
|
||||
color?: KpiColor;
|
||||
/**
|
||||
* Опциональная подпись под значением (caption-токен 12/16, --fg-tertiary).
|
||||
* Видимая (не hover) — для честных квалификаторов вроде «оценочный».
|
||||
*/
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export function KpiCard({ value, label, color = "neutral" }: Props) {
|
||||
export function KpiCard({ value, label, color = "neutral", caption }: Props) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -47,6 +52,18 @@ export function KpiCard({ value, label, color = "neutral" }: Props) {
|
|||
>
|
||||
{value}
|
||||
</div>
|
||||
{caption ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: "16px",
|
||||
color: "var(--fg-tertiary)",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
/**
|
||||
* 6.1 Прогноз по горизонтам — compact table over future_market.forecasts_by_horizon.
|
||||
*
|
||||
* Columns: горизонт (мес) · индекс дефицита (color-coded) · месяцы предложения ·
|
||||
* Columns: горизонт (мес) · индекс дефицита (color-coded) ·
|
||||
* прогноз спроса/предложения · ставка (% годовых) · фраза чувствительности к ставке.
|
||||
*
|
||||
* MOI (months_of_inventory) НЕ выводим по горизонтам: по определению он не зависит
|
||||
* от горизонта проекции, поэтому колонка дублировала одно и то же число 4 раза
|
||||
* (выглядело сломанным). MOI показан один раз в Section6Forecast key_numbers.
|
||||
*
|
||||
* Deficit semantics (HARD, see frontend rules):
|
||||
* deficit_index > 0 → недонасыщенность (повод строить) → success/green
|
||||
* deficit_index < 0 → затоварка (предложения > спроса) → warn/danger
|
||||
|
|
@ -15,7 +19,6 @@
|
|||
import { Badge } from "@/components/ui/Badge";
|
||||
import type { DemandSupplyForecast } from "@/types/forecast";
|
||||
import {
|
||||
DEFICIT_BALANCE_EPS,
|
||||
deficitVariant,
|
||||
deficitWord,
|
||||
fmtNum,
|
||||
|
|
@ -77,12 +80,6 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
<tr>
|
||||
<th style={TH_STYLE}>Горизонт</th>
|
||||
<th style={TH_STYLE}>Индекс дефицита</th>
|
||||
<th
|
||||
style={{ ...TH_STYLE, textAlign: "right" }}
|
||||
title="Во сколько месяцев распродаётся текущий объём при базовом темпе"
|
||||
>
|
||||
Мес. предложения
|
||||
</th>
|
||||
<th style={{ ...TH_STYLE, textAlign: "right" }}>
|
||||
Прогноз спроса, квартир
|
||||
</th>
|
||||
|
|
@ -95,7 +92,6 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
<tbody>
|
||||
{rows.map((f) => {
|
||||
const di = f.deficit_index;
|
||||
const balanced = Math.abs(di) < DEFICIT_BALANCE_EPS;
|
||||
const isTarget = f.horizon_months === targetHorizon;
|
||||
return (
|
||||
<tr
|
||||
|
|
@ -142,9 +138,6 @@ export function ForecastHorizonsBlock({ forecasts, targetHorizon }: Props) {
|
|||
{deficitWord(di)}
|
||||
</span>
|
||||
</td>
|
||||
<td style={NUM_TD}>
|
||||
{balanced ? "—" : fmtNum(f.months_of_inventory, 1)}
|
||||
</td>
|
||||
<td style={NUM_TD}>
|
||||
{Math.round(f.projected_demand_units).toLocaleString("ru")}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -202,13 +202,15 @@ export function Section1ParcelInfo({ cad }: Props) {
|
|||
: "—";
|
||||
const scoreStr = `${data.score}`;
|
||||
|
||||
// Score color
|
||||
// Score color — пороги выровнены под калиброванную 0–40 шкалу _score_label
|
||||
// (#1871 P2: было 0–100, давало red на хорошем ~30/40 участке).
|
||||
// <5 плохо · 5–15 средне · 15–25 хорошо · ≥25 отлично.
|
||||
const scoreColor =
|
||||
data.score >= 80
|
||||
data.score >= 25
|
||||
? "green"
|
||||
: data.score >= 65
|
||||
: data.score >= 15
|
||||
? "blue"
|
||||
: data.score >= 45
|
||||
: data.score >= 5
|
||||
? "amber"
|
||||
: "red";
|
||||
|
||||
|
|
@ -276,7 +278,12 @@ export function Section1ParcelInfo({ cad }: Props) {
|
|||
<KpiCard value={areaHa} label="Площадь" color="neutral" />
|
||||
<KpiCard value={district} label="Район" color="neutral" />
|
||||
<KpiCard value={medianPrice} label="Медиана рынка" color="blue" />
|
||||
<KpiCard value={scoreStr} label="Инвест-балл" color={scoreColor} />
|
||||
<KpiCard
|
||||
value={scoreStr}
|
||||
label="POI-скор · 0–40"
|
||||
caption="оценочный"
|
||||
color={scoreColor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 2-column grid: map (left) + EGRN + POI (right) */}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const METRIC_ROWS: MetricRow[] = [
|
|||
},
|
||||
{
|
||||
key: "score",
|
||||
label: "Инвест-балл",
|
||||
label: "POI-скор · 0–40",
|
||||
winner: "high",
|
||||
value: (c) => c.score ?? null,
|
||||
render: (c) => (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue