Лучшие планировки: в «покрытие P% (Y из N комплексов)» N считается одинаково во всех ветках #3570
4 changed files with 46 additions and 8 deletions
|
|
@ -627,7 +627,7 @@ class LayoutDataQuality(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
objects_with_velocity_data: int # число комплексов с velocity-данными
|
objects_with_velocity_data: int # число комплексов с velocity-данными
|
||||||
objects_total_in_radius: int # число комплексов (сгруппированных obj_id)
|
objects_total_in_radius: int # число комплексов после exclude/filter (знаменатель coverage)
|
||||||
raw_objects_total: int # сырое число obj_id в радиусе (до группировки)
|
raw_objects_total: int # сырое число obj_id в радиусе (до группировки)
|
||||||
# Issue #2177 шаг 2b (core-attribution): сколько комплексов покрыто ИМЕННО по
|
# Issue #2177 шаг 2b (core-attribution): сколько комплексов покрыто ИМЕННО по
|
||||||
# нормализованному ядру имени (project_name-матч), а НЕ через mapping-obj_id —
|
# нормализованному ядру имени (project_name-матч), а НЕ через mapping-obj_id —
|
||||||
|
|
|
||||||
|
|
@ -1109,9 +1109,6 @@ def get_best_layouts(
|
||||||
id_row_dicts: list[dict[str, Any]] = [dict(r) for r in id_rows]
|
id_row_dicts: list[dict[str, Any]] = [dict(r) for r in id_rows]
|
||||||
all_obj_ids: list[int] = [int(r["obj_id"]) for r in id_row_dicts]
|
all_obj_ids: list[int] = [int(r["obj_id"]) for r in id_row_dicts]
|
||||||
raw_objects_total_in_radius = len(all_obj_ids)
|
raw_objects_total_in_radius = len(all_obj_ids)
|
||||||
# Число комплексов ДО exclude/filter — знаменатель для empty-ответа (совместимо
|
|
||||||
# со старой семантикой: пустой ответ отражал полный радиус, а не post-filter 0).
|
|
||||||
groups_total_pre_filter = len(group_radius_objects(id_row_dicts))
|
|
||||||
|
|
||||||
# Применить exclude / filter из request (и к obj_ids, и к строкам для группировки)
|
# Применить exclude / filter из request (и к obj_ids, и к строкам для группировки)
|
||||||
exclude_set = set(request.exclude_competitor_obj_ids)
|
exclude_set = set(request.exclude_competitor_obj_ids)
|
||||||
|
|
@ -1132,13 +1129,16 @@ def get_best_layouts(
|
||||||
all_obj_ids = [oid for oid in all_obj_ids if _keep(oid)]
|
all_obj_ids = [oid for oid in all_obj_ids if _keep(oid)]
|
||||||
|
|
||||||
# Группируем отфильтрованные obj_id в комплексы — знаменатель coverage.
|
# Группируем отфильтрованные obj_id в комплексы — знаменатель coverage.
|
||||||
|
# objects_total_in_radius во ВСЕХ ветках = len(complex_groups) (после фильтра):
|
||||||
|
# UI и PDF печатают его как N в «покрытие P% (Y из N комплексов)», а P считается
|
||||||
|
# по отфильтрованным (#2464). Число до фильтра — в raw_objects_total.
|
||||||
complex_groups = group_radius_objects(id_row_dicts)
|
complex_groups = group_radius_objects(id_row_dicts)
|
||||||
|
|
||||||
if not all_obj_ids:
|
if not all_obj_ids:
|
||||||
return _empty_response(
|
return _empty_response(
|
||||||
radius_km=request.radius_km,
|
radius_km=request.radius_km,
|
||||||
time_window=request.time_window,
|
time_window=request.time_window,
|
||||||
objects_total_in_radius=groups_total_pre_filter,
|
objects_total_in_radius=len(complex_groups),
|
||||||
raw_objects_total=raw_objects_total_in_radius,
|
raw_objects_total=raw_objects_total_in_radius,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,10 @@ def test_exclude_competitor_obj_ids_filter() -> None:
|
||||||
# После исключения obj_id=20 список пуст → пустой ответ
|
# После исключения obj_id=20 список пуст → пустой ответ
|
||||||
assert body["top_layouts"] == []
|
assert body["top_layouts"] == []
|
||||||
assert body["data_quality"]["confidence"] == "low"
|
assert body["data_quality"]["confidence"] == "low"
|
||||||
# objects_total_in_radius = 1 (до исключения)
|
# objects_total_in_radius — знаменатель coverage, после исключения 0 (#2464);
|
||||||
assert body["data_quality"]["objects_total_in_radius"] == 1
|
# число obj_id в радиусе до исключения — raw_objects_total
|
||||||
|
assert body["data_quality"]["objects_total_in_radius"] == 0
|
||||||
|
assert body["data_quality"]["raw_objects_total"] == 1
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,43 @@ def test_exclude_competitor_obj_ids() -> None:
|
||||||
resp = get_best_layouts(db, CAD_NUM, req)
|
resp = get_best_layouts(db, CAD_NUM, req)
|
||||||
|
|
||||||
assert resp.top_layouts == []
|
assert resp.top_layouts == []
|
||||||
assert resp.data_quality.objects_total_in_radius == 1
|
# после исключения комплексов 0 (знаменатель coverage), в радиусе было 1 obj_id
|
||||||
|
assert resp.data_quality.objects_total_in_radius == 0
|
||||||
|
assert resp.data_quality.raw_objects_total == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("filter_ids", "vel_obj_ids", "expected_total", "expected_with_data"),
|
||||||
|
[
|
||||||
|
([99], None, 0, 0), # фильтр не оставил никого → ранний пустой ответ
|
||||||
|
([20], None, 1, 0), # остался 1 комплекс без velocity → пустой ответ
|
||||||
|
([20], [20], 1, 1), # остался 1 комплекс с velocity → штатная ветка
|
||||||
|
],
|
||||||
|
ids=["all-filtered", "no-velocity", "normal"],
|
||||||
|
)
|
||||||
|
def test_objects_total_is_coverage_denominator_in_every_branch(
|
||||||
|
filter_ids: list[int],
|
||||||
|
vel_obj_ids: list[int] | None,
|
||||||
|
expected_total: int,
|
||||||
|
expected_with_data: int,
|
||||||
|
) -> None:
|
||||||
|
"""#2464 (best_layouts:1113): objects_total_in_radius значит одно во всех ветках.
|
||||||
|
|
||||||
|
UI и PDF печатают «покрытие P% (Y из N комплексов)», значит N обязан быть
|
||||||
|
знаменателем P, а P считается по комплексам ПОСЛЕ filter. Раньше ранний пустой
|
||||||
|
ответ отдавал число ДО фильтра (2), две другие ветки — после.
|
||||||
|
"""
|
||||||
|
id_rows = [_obj_id_row(20), _obj_id_row(30)]
|
||||||
|
vel_rows = [_vel_row("2", deals_window=30.0, obj_ids=vel_obj_ids)] if vel_obj_ids else []
|
||||||
|
db = _make_db(coord=_coord_row(), id_rows=id_rows, vel_rows=vel_rows)
|
||||||
|
resp = get_best_layouts(db, CAD_NUM, _request(filter_competitor_obj_ids=filter_ids))
|
||||||
|
|
||||||
|
dq = resp.data_quality
|
||||||
|
assert dq.objects_total_in_radius == expected_total
|
||||||
|
assert dq.objects_with_velocity_data == expected_with_data
|
||||||
|
assert dq.raw_objects_total == 2 # число до фильтра — отдельным полем
|
||||||
|
expected_pct = expected_with_data / expected_total * 100.0 if expected_total else 0.0
|
||||||
|
assert dq.velocity_coverage_pct == pytest.approx(expected_pct)
|
||||||
|
|
||||||
|
|
||||||
# ── Тест: total_sold_in_window совпадает с deals_window ──────────────────────
|
# ── Тест: total_sold_in_window совпадает с deals_window ──────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue