gendesign/backend/app/services/site_finder/layout_signature.py
lekss361 b9259cd8ed fix(sf-08): add euro-1/euro-2 room_bucket for small-format rooms=2 flats
DOM.РФ API classifies euro-format flats (26-50м²) as rooms=2.
_SUPPLY_BATCH_SQL now maps:
  rooms=2 + area<35 → 'euro-1'
  rooms=2 + area<50 → 'euro-2'
before the generic rooms IN (1,2,3) branch.

Audit: 4 140 units rooms=2 + area<35 in domrf_kn_flats were inflating
the supply count for genuine 2-room apartments (median 57м²).
euro buckets have no velocity counterpart in objective_corpus_room_month
so they are filtered out of top_layouts by min_velocity.

Also extends RoomBucket Literal and room_bucket_from_flat() in
layout_signature.py with the new euro-1/euro-2 values.

Closes #271 item 8
2026-05-17 14:38:48 +03:00

66 lines
2 KiB
Python

"""Layout signature extraction для Issue #113 (Phase 2.1 minimal).
Без `layout_type` / `balcony_count` — ждут B2B Объектив (#52).
"""
from __future__ import annotations
from typing import Literal
RoomBucket = Literal["studio", "euro-1", "euro-2", "1", "2", "3", "4+"]
AreaBin = Literal["<25", "25-40", "40-60", "60-80", "80-100", "100+"]
def room_bucket_from_flat(
rooms: int | None,
flat_type: str | None,
is_studio: bool | None,
total_area: float | None = None,
) -> RoomBucket:
"""Determine room_bucket из kn_flats полей.
Priority:
1. is_studio=True OR flat_type='Квартира-студия'"studio"
2. rooms=0 (без is_studio) → "studio"
3. Fix SF-08: rooms=2 + area<35 → "euro-1" (DOM.РФ маркирует малогабаритные как 2-комн)
4. Fix SF-08: rooms=2 + area<50 → "euro-2" (евро-двушки 35-50м²)
5. rooms IN (1, 2, 3) → str(rooms)
6. rooms >= 4 → "4+"
7. fallback на "1" (rooms is None и не studio)
"""
if is_studio is True or flat_type == "Квартира-студия":
return "studio"
if rooms is None:
return "1"
if rooms == 0:
return "studio"
if rooms == 2 and total_area is not None:
if total_area < 35.0:
return "euro-1"
if total_area < 50.0:
return "euro-2"
if rooms >= 4:
return "4+"
if rooms in (1, 2, 3):
return str(rooms) # type: ignore[return-value]
return "1"
def area_bin(area_m2: float) -> AreaBin:
"""Bucket площади per ARN-buckets (НСПД)."""
if area_m2 < 25.0:
return "<25"
if area_m2 < 40.0:
return "25-40"
if area_m2 < 60.0:
return "40-60"
if area_m2 < 80.0:
return "60-80"
if area_m2 < 100.0:
return "80-100"
return "100+"
def layout_signature(room_bucket_val: RoomBucket, area_bin_val: AreaBin) -> str:
"""Deterministic string-signature для группировки."""
return f"{room_bucket_val}__{area_bin_val}"