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 (медиана
|
# лежало 16 801 лот региона 50 (медиана 251 295 ₽/м²) и 82 719 лотов региона 77 (медиана
|
||||||
# 458 208) — примесь заниженная городскую медиану до 422 222 (−7.9%). У областных строк
|
# 458 208) — примесь заниженная городскую медиану до 422 222 (−7.9%). У областных строк
|
||||||
# `city IS NULL`, фильтровать по имени города нельзя — только по region_code.
|
# `city IS NULL`, фильтровать по имени города нельзя — только по region_code.
|
||||||
# `OR region_code IS NULL` — НЕ дыра для Москвы (внутри её bbox_product_core таких строк
|
#
|
||||||
# сегодня 0, проверено на проде), а сохранение прежнего поведения региона 66: там есть
|
# СТРОГОЕ равенство, БЕЗ `OR region_code IS NULL`: строка без региона не считается ни за
|
||||||
# горстка NULL-region_code листингов (untagged legacy до бэкафилла), которые старый
|
# один регион. Второй прод-замер (13.09.2026, полный набор фильтров модуля — is_active,
|
||||||
# запрос (без фильтра по региону вообще) всегда учитывал. Без OR-ветки эта правка
|
# price sanity, geo_precision, свежесть 14 дней, сегмент, bbox ядра 66): 15 858 строк
|
||||||
# незаметно сдвинула бы медиану 66, хотя тикет требует byte-for-byte.
|
# проходят фильтры, из них 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(
|
_MEDIAN_PPM2_LOCAL_SQL = text(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -236,7 +242,7 @@ _MEDIAN_PPM2_LOCAL_SQL = text(
|
||||||
AND (geo_precision IS DISTINCT FROM 'city')
|
AND (geo_precision IS DISTINCT FROM 'city')
|
||||||
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
||||||
AND (listing_segment IS NULL OR listing_segment = 'vtorichka')
|
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 lat BETWEEN CAST(:bbox_south AS double precision)
|
||||||
AND CAST(:bbox_north AS double precision)
|
AND CAST(:bbox_north AS double precision)
|
||||||
AND lon BETWEEN CAST(:bbox_west 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 (geo_precision IS DISTINCT FROM 'city')
|
||||||
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
AND scraped_at > NOW() - (:fresh_days || ' days')::interval
|
||||||
AND (listing_segment IS NULL OR listing_segment = 'vtorichka')
|
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 lat BETWEEN CAST(:bbox_south AS double precision)
|
||||||
AND CAST(:bbox_north AS double precision)
|
AND CAST(:bbox_north AS double precision)
|
||||||
AND lon BETWEEN CAST(:bbox_west 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):
|
for sql in (lc._MEDIAN_PPM2_LOCAL_SQL, lc._MEDIAN_PPM2_CITYWIDE_SQL):
|
||||||
sql_text = str(sql.text)
|
sql_text = str(sql.text)
|
||||||
assert "region_code = CAST(:region_code AS integer)" in 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:
|
def test_local_and_citywide_median_bind_region_code_of_the_given_region() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue