fix(tradein): убрать OR region_code IS NULL из median-SQL location_index
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 19s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m33s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 19s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m33s
Строгое region_code = CAST(:region_code AS integer), без OR-ветки на NULL. Второй прод-замер (13.09.2026, полный набор фильтров модуля — is_active, price sanity, geo_precision, свежесть 14 дней, сегмент, bbox ядра 66): 15 858 строк проходят фильтры, из них 3 с NULL region_code. Медиана с ними 151 101 ₽/м², без них 151 103 — сдвиг 2 рубля на 151 тысяче (0.001%), для региона 66 статистически ничто. Мягкий OR NULL был бы лазейкой того же класса, что и сам дефект: в listings 16 930 NULL-region строк Avito, 24 из них уже лежат в bbox Москвы (сегодня не проходят по свежести, но могут завтра) — OR NULL тихо открыл бы им дорогу обратно в московскую медиану тем же путём, каким туда раньше попадала Московская область. Тесты: test_median_queries_do_not_admit_null_region_code (обе median-SQL не содержат "OR region_code" / "region_code IS NULL"), комментарий перед SQL переписан под новое решение с этими числами.
This commit is contained in:
parent
def21dc796
commit
3fae2b4ed8
2 changed files with 28 additions and 10 deletions
|
|
@ -218,11 +218,17 @@ def _pct_deviation(local_median_ppm2: float, city_median_ppm2: float) -> float:
|
|||
# лежало 16 801 лот региона 50 (медиана 251 295 ₽/м²) и 82 719 лотов региона 77 (медиана
|
||||
# 458 208) — примесь заниженная городскую медиану до 422 222 (−7.9%). У областных строк
|
||||
# `city IS NULL`, фильтровать по имени города нельзя — только по region_code.
|
||||
# `OR region_code IS NULL` — НЕ дыра для Москвы (внутри её bbox_product_core таких строк
|
||||
# сегодня 0, проверено на проде), а сохранение прежнего поведения региона 66: там есть
|
||||
# горстка NULL-region_code листингов (untagged legacy до бэкафилла), которые старый
|
||||
# запрос (без фильтра по региону вообще) всегда учитывал. Без OR-ветки эта правка
|
||||
# незаметно сдвинула бы медиану 66, хотя тикет требует byte-for-byte.
|
||||
#
|
||||
# СТРОГОЕ равенство, БЕЗ `OR region_code IS NULL`: строка без региона не считается ни за
|
||||
# один регион. Второй прод-замер (13.09.2026, полный набор фильтров модуля — is_active,
|
||||
# price sanity, geo_precision, свежесть 14 дней, сегмент, bbox ядра 66): 15 858 строк
|
||||
# проходят фильтры, из них 3 с NULL region_code; медиана с ними 151 101 ₽/м², без них
|
||||
# 151 103 — сдвиг 2 рубля на 151 тысяче (0.001%), статистически ничто для 66. Взамен —
|
||||
# `region_code IS NULL` НЕ постоянная лазейка того же класса, что и сам дефект: в
|
||||
# `listings` 16 930 NULL-region строк Avito, 24 из них лежат в bbox Москвы и сегодня не
|
||||
# проходят по свежести — но могут завтра. Мягкий `OR NULL` тихо открыл бы им дорогу
|
||||
# обратно в московскую медиану ровно тем же путём, каким туда попадала Московская
|
||||
# область. Три рубля на 66 — цена закрытия дыры, а не повод её оставить.
|
||||
_MEDIAN_PPM2_LOCAL_SQL = text(
|
||||
"""
|
||||
SELECT
|
||||
|
|
@ -236,7 +242,7 @@ _MEDIAN_PPM2_LOCAL_SQL = text(
|
|||
AND (geo_precision IS DISTINCT FROM 'city')
|
||||
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
||||
AND (listing_segment IS NULL OR listing_segment = 'vtorichka')
|
||||
AND (region_code = CAST(:region_code AS integer) OR region_code IS NULL)
|
||||
AND region_code = CAST(:region_code AS integer)
|
||||
AND lat BETWEEN CAST(:bbox_south AS double precision)
|
||||
AND CAST(:bbox_north AS double precision)
|
||||
AND lon BETWEEN CAST(:bbox_west AS double precision)
|
||||
|
|
@ -262,7 +268,7 @@ _MEDIAN_PPM2_CITYWIDE_SQL = text(
|
|||
AND (geo_precision IS DISTINCT FROM 'city')
|
||||
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
||||
AND (listing_segment IS NULL OR listing_segment = 'vtorichka')
|
||||
AND (region_code = CAST(:region_code AS integer) OR region_code IS NULL)
|
||||
AND region_code = CAST(:region_code AS integer)
|
||||
AND lat BETWEEN CAST(:bbox_south AS double precision)
|
||||
AND CAST(:bbox_north AS double precision)
|
||||
AND lon BETWEEN CAST(:bbox_west AS double precision)
|
||||
|
|
|
|||
|
|
@ -161,9 +161,21 @@ def test_median_queries_filter_by_region_code() -> None:
|
|||
for sql in (lc._MEDIAN_PPM2_LOCAL_SQL, lc._MEDIAN_PPM2_CITYWIDE_SQL):
|
||||
sql_text = str(sql.text)
|
||||
assert "region_code = CAST(:region_code AS integer)" in sql_text
|
||||
# NULL region_code остаётся в пуле — иначе региона 66 (где часть legacy-листингов
|
||||
# не размечена) сместился бы, хотя тикет требует byte-for-byte для 66.
|
||||
assert "OR region_code IS NULL" in sql_text
|
||||
|
||||
|
||||
def test_median_queries_do_not_admit_null_region_code() -> None:
|
||||
"""Строгое равенство, БЕЗ `OR region_code IS NULL` — строка без региона не считается
|
||||
ни за один регион. Второй прод-замер (13.09.2026, полный набор фильтров модуля, bbox
|
||||
ядра 66): 15 858 строк проходят фильтры, 3 из них с NULL region_code; медиана с ними
|
||||
151 101 ₽/м², без них 151 103 — сдвиг 2 рубля на 151 тысяче (0.001%), для 66
|
||||
статистически ничто. Мягкий `OR NULL` был бы лазейкой того же класса, что и сам
|
||||
дефект: в `listings` 16 930 NULL-region строк Avito, 24 из них уже лежат в bbox
|
||||
Москвы (сегодня не проходят по свежести, но могут завтра) — `OR NULL` тихо открыл бы
|
||||
им дорогу обратно в московскую медиану."""
|
||||
for sql in (lc._MEDIAN_PPM2_LOCAL_SQL, lc._MEDIAN_PPM2_CITYWIDE_SQL):
|
||||
sql_text = str(sql.text)
|
||||
assert "region_code IS NULL" not in sql_text
|
||||
assert "OR region_code" not in sql_text
|
||||
|
||||
|
||||
def test_local_and_citywide_median_bind_region_code_of_the_given_region() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue