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