Merge pull request 'fix(analytics): align portfolio series to area buckets in quartirography (#1406)' (#1675) from fix/quartirography-area-bucket-1406 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
This commit is contained in:
commit
3ffba9f8a8
2 changed files with 76 additions and 41 deletions
|
|
@ -79,46 +79,83 @@ def market_pulse(db: Session, region_code: int = 66) -> list[dict[str, Any]]:
|
||||||
|
|
||||||
|
|
||||||
def quartirography(db: Session, source: str, region_id: int = 66) -> list[dict[str, Any]]:
|
def quartirography(db: Session, source: str, region_id: int = 66) -> list[dict[str, Any]]:
|
||||||
"""source: 'portfolio' (что строится) or 'deals' (реально покупают)."""
|
"""source: 'portfolio' (что строится) or 'deals' (реально покупают).
|
||||||
|
|
||||||
|
Оба источника возвращают одни и те же 5 area-бакетов, выровненных с осью
|
||||||
|
QuartirographyChart ("Студии 15-30" / "1-к 30-45" / "2-к 45-60" /
|
||||||
|
"3-к 60-80" / "80+ м²").
|
||||||
|
|
||||||
|
Портфель: domrf_flat_area_distribution (RF-wide snapshot, region_id=0 т.к.
|
||||||
|
API DOM.РФ игнорирует ?regionId на этом эндпоинте). Бакеты DOM.РФ (FROM_0_TO_25
|
||||||
|
… FROM_100) маппируются на chart-бакеты по средней площади:
|
||||||
|
FROM_0_TO_25 (~22 м²) → "Студии 15-30"
|
||||||
|
FROM_25_TO_35 (~31 м², мелкие однушки) → "1-к 30-45"
|
||||||
|
FROM_35_TO_45 (~40 м²) → "1-к 30-45"
|
||||||
|
FROM_45_TO_55 (~50 м²) → "2-к 45-60"
|
||||||
|
FROM_55_TO_70 (~62 м²) → "3-к 60-80"
|
||||||
|
FROM_70_TO_85 (~77 м²) → "3-к 60-80"
|
||||||
|
FROM_85_TO_100 (~92 м²) → "80+ м²"
|
||||||
|
FROM_100 (100+ м²) → "80+ м²"
|
||||||
|
"""
|
||||||
if source == "portfolio":
|
if source == "portfolio":
|
||||||
rows = (
|
rows = (
|
||||||
db.execute(
|
db.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
SELECT room_count_type, flat_count, area_sqm, percent
|
WITH latest AS (
|
||||||
FROM domrf_region_aggregates
|
SELECT MAX(snapshot_date) AS snap
|
||||||
WHERE region_id = :region_id
|
FROM domrf_flat_area_distribution
|
||||||
AND snapshot_date = (
|
WHERE region_id = 0
|
||||||
SELECT MAX(snapshot_date)
|
),
|
||||||
FROM domrf_region_aggregates
|
mapped AS (
|
||||||
WHERE region_id = :region_id
|
SELECT
|
||||||
)
|
CASE area_bucket
|
||||||
AND room_count_type <> 'TOTAL'
|
WHEN 'FROM_0_TO_25' THEN '1-Студия'
|
||||||
ORDER BY CASE room_count_type
|
WHEN 'FROM_25_TO_35' THEN '2-1-к'
|
||||||
WHEN 'ONE' THEN 1
|
WHEN 'FROM_35_TO_45' THEN '2-1-к'
|
||||||
WHEN 'TWO' THEN 2
|
WHEN 'FROM_45_TO_55' THEN '3-2-к'
|
||||||
WHEN 'THREE' THEN 3
|
WHEN 'FROM_55_TO_70' THEN '4-3-к'
|
||||||
WHEN 'FOUR' THEN 4
|
WHEN 'FROM_70_TO_85' THEN '4-3-к'
|
||||||
END
|
WHEN 'FROM_85_TO_100' THEN '5-80+ м²'
|
||||||
|
WHEN 'FROM_100' THEN '5-80+ м²'
|
||||||
|
END AS chart_bucket,
|
||||||
|
flat_count,
|
||||||
|
area_sqm
|
||||||
|
FROM domrf_flat_area_distribution
|
||||||
|
CROSS JOIN latest
|
||||||
|
WHERE region_id = 0
|
||||||
|
AND room_count_type = 'TOTAL'
|
||||||
|
AND snapshot_date = latest.snap
|
||||||
|
)
|
||||||
|
SELECT chart_bucket,
|
||||||
|
SUM(flat_count)::bigint AS flat_count,
|
||||||
|
SUM(area_sqm) AS area_sqm
|
||||||
|
FROM mapped
|
||||||
|
WHERE chart_bucket IS NOT NULL
|
||||||
|
GROUP BY chart_bucket
|
||||||
|
ORDER BY chart_bucket
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
{"region_id": region_id},
|
{},
|
||||||
)
|
)
|
||||||
.mappings()
|
.mappings()
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
_pretty = {
|
||||||
|
"1-Студия": "Студии 15-30",
|
||||||
|
"2-1-к": "1-к 30-45",
|
||||||
|
"3-2-к": "2-к 45-60",
|
||||||
|
"4-3-к": "3-к 60-80",
|
||||||
|
"5-80+ м²": "80+ м²",
|
||||||
|
}
|
||||||
|
total_flats = sum(r["flat_count"] or 0 for r in rows) or 1
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"bucket": {
|
"bucket": _pretty[r["chart_bucket"]],
|
||||||
"ONE": "1-к",
|
"flat_count": int(r["flat_count"] or 0),
|
||||||
"TWO": "2-к",
|
|
||||||
"THREE": "3-к",
|
|
||||||
"FOUR": "4+",
|
|
||||||
}.get(r["room_count_type"], r["room_count_type"]),
|
|
||||||
"flat_count": r["flat_count"],
|
|
||||||
"area_sqm": _f(r["area_sqm"]),
|
"area_sqm": _f(r["area_sqm"]),
|
||||||
"percent": r["percent"],
|
"percent": round((r["flat_count"] or 0) * 100 / total_flats, 1),
|
||||||
"avg_area": _f(r["area_sqm"] / r["flat_count"]) if r["flat_count"] else None,
|
"avg_area": (_f(r["area_sqm"] / r["flat_count"]) if r["flat_count"] else None),
|
||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
|
|
@ -2632,9 +2669,7 @@ def recommend_mix(
|
||||||
else:
|
else:
|
||||||
# Rosreestr fallback aggregate: district+class deals / months / n_comp =
|
# Rosreestr fallback aggregate: district+class deals / months / n_comp =
|
||||||
# среднерыночный темп одного ЖК района/класса (срок ~12-24 мес).
|
# среднерыночный темп одного ЖК района/класса (срок ~12-24 мес).
|
||||||
market_vel_pm = (
|
market_vel_pm = (total_deals / max(effective_window, 1) / n_comp) if total_deals else 0.0
|
||||||
(total_deals / max(effective_window, 1) / n_comp) if total_deals else 0.0
|
|
||||||
)
|
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"Нет objective-данных для района/класса — темп по rosreestr ÷ "
|
f"Нет objective-данных для района/класса — темп по rosreestr ÷ "
|
||||||
f"{n_comp} активных ЖК района/класса (грубее, срок ориентировочный)."
|
f"{n_comp} активных ЖК района/класса (грубее, срок ориентировочный)."
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,16 @@ export function QuartirographyChart() {
|
||||||
"3-к 60-80",
|
"3-к 60-80",
|
||||||
"80+ м²",
|
"80+ м²",
|
||||||
];
|
];
|
||||||
// DOM.РФ portfolio source (domrf_region_aggregates) has no studio segment
|
// Portfolio is now served from domrf_flat_area_distribution with the same
|
||||||
// (room_count_type only ONE/TWO/THREE/FOUR), so "Студии 15-30" has no
|
// area bucket keys as the deals series — direct lookup by bucket label.
|
||||||
// counterpart here. Map it to null → ECharts draws no bar (N/A) instead of
|
// If a bucket is absent in the source (e.g. studio data gap), percent
|
||||||
// a misleading 0% grey bar.
|
// falls back to null so ECharts draws no bar rather than a misleading 0%.
|
||||||
const portfolioMap: Record<string, number | null> = {
|
const portfolioMap: Record<string, number | null> = Object.fromEntries(
|
||||||
"Студии 15-30": null,
|
buckets.map((b) => {
|
||||||
"1-к 30-45": portfolioRows.find((r) => r.bucket === "1-к")?.percent ?? 0,
|
const row = portfolioRows.find((r) => r.bucket === b);
|
||||||
"2-к 45-60": portfolioRows.find((r) => r.bucket === "2-к")?.percent ?? 0,
|
return [b, row != null ? (row.percent ?? null) : null];
|
||||||
"3-к 60-80": portfolioRows.find((r) => r.bucket === "3-к")?.percent ?? 0,
|
}),
|
||||||
"80+ м²": portfolioRows.find((r) => r.bucket === "4+")?.percent ?? 0,
|
);
|
||||||
};
|
|
||||||
const dealsPercents = buckets.map(
|
const dealsPercents = buckets.map(
|
||||||
(b) => dealsRows.find((r) => r.bucket === b)?.percent ?? 0,
|
(b) => dealsRows.find((r) => r.bucket === b)?.percent ?? 0,
|
||||||
);
|
);
|
||||||
|
|
@ -43,7 +42,8 @@ export function QuartirographyChart() {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
axisPointer: { type: "shadow" },
|
axisPointer: { type: "shadow" },
|
||||||
valueFormatter: (v: number | null) => (v == null ? "нет данных" : `${v}%`),
|
valueFormatter: (v: number | null) =>
|
||||||
|
v == null ? "нет данных" : `${v}%`,
|
||||||
},
|
},
|
||||||
legend: { data: ["Что строится (портфель)", "Что покупают (ДДУ)"] },
|
legend: { data: ["Что строится (портфель)", "Что покупают (ДДУ)"] },
|
||||||
grid: { left: 110, right: 32, top: 40, bottom: 28 },
|
grid: { left: 110, right: 32, top: 40, bottom: 28 },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue