Оценка: дедуп склеивает улицы вида «40-летия Комсомола», пороги Tier H — в настройки для бэктеста #3552
3 changed files with 103 additions and 35 deletions
|
|
@ -7328,15 +7328,16 @@ _DEDUP_STREET_TYPE_ALT = "|".join(
|
||||||
)
|
)
|
||||||
# «<тип>[.] <название>»: название = опциональная ведущая цифро-группа + 1-3
|
# «<тип>[.] <название>»: название = опциональная ведущая цифро-группа + 1-3
|
||||||
# буквенных слова, до запятой/цифры. Ведущая цифра нужна для нумерованных улиц
|
# буквенных слова, до запятой/цифры. Ведущая цифра нужна для нумерованных улиц
|
||||||
# («8 Марта», «40 лет Октября», «22 Партсъезда»); она разрешена ТОЛЬКО перед
|
# («8 Марта», «40 лет Октября», «22 Партсъезда»; #2291 класс 4 — и через дефис:
|
||||||
# буквенным словом (?:\d+\s+)?[а-яё]…, поэтому НОМЕР ДОМА (число ПОСЛЕ названия,
|
# «40-летия Комсомола», «22-го Партсъезда»); она разрешена ТОЛЬКО перед
|
||||||
|
# буквенным словом (?:\d+(?:\s+|-))?[а-яё]…, поэтому НОМЕР ДОМА (число ПОСЛЕ названия,
|
||||||
# не сопровождаемое буквой) в токен не попадает: «Белинского 86» → «белинского».
|
# не сопровождаемое буквой) в токен не попадает: «Белинского 86» → «белинского».
|
||||||
# #2269 (б): тип-суффикс может быть приклеен запятой/точкой без пробела
|
# #2269 (б): тип-суффикс может быть приклеен запятой/точкой без пробела
|
||||||
# («ул.,6к1»): после `\.?` разрешаем ЛИБО пробел (обычный формат), ЛИБО запятую
|
# («ул.,6к1»): после `\.?` разрешаем ЛИБО пробел (обычный формат), ЛИБО запятую
|
||||||
# (→ следом номер дома). Ветка «тип ПЕРЕД именем» — только для формата с пробелом.
|
# (→ следом номер дома). Ветка «тип ПЕРЕД именем» — только для формата с пробелом.
|
||||||
_DEDUP_STREET_NAME_RE = re.compile(
|
_DEDUP_STREET_NAME_RE = re.compile(
|
||||||
rf"(?:^|[,\s])(?:{_DEDUP_STREET_TYPE_ALT})\.?\s+"
|
rf"(?:^|[,\s])(?:{_DEDUP_STREET_TYPE_ALT})\.?\s+"
|
||||||
r"((?:\d+\s+)?[а-яё][а-яё\-]+(?:\s+[а-яё][а-яё\-]+){0,2})",
|
r"((?:\d+(?:\s+|-))?[а-яё][а-яё\-]+(?:\s+[а-яё][а-яё\-]+){0,2})",
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
# #2269 (а): «<название> <тип>» — тип-слово ПОСЛЕ имени (формат domklik:
|
# #2269 (а): «<название> <тип>» — тип-слово ПОСЛЕ имени (формат domklik:
|
||||||
|
|
@ -7348,7 +7349,7 @@ _DEDUP_STREET_NAME_RE = re.compile(
|
||||||
# Рассветная». Тип НЕ входит в захват (group 1) — токен байт-в-байт совпадает.
|
# Рассветная». Тип НЕ входит в захват (group 1) — токен байт-в-байт совпадает.
|
||||||
_DEDUP_STREET_NAME_SUFFIX_RE = re.compile(
|
_DEDUP_STREET_NAME_SUFFIX_RE = re.compile(
|
||||||
r"(?:^|[,\s])"
|
r"(?:^|[,\s])"
|
||||||
r"((?:\d+\s+)?[а-яё][а-яё\-]+(?:\s+[а-яё][а-яё\-]+){0,2})\s+"
|
r"((?:\d+(?:\s+|-))?[а-яё][а-яё\-]+(?:\s+[а-яё][а-яё\-]+){0,2})\s+"
|
||||||
rf"(?:{_DEDUP_STREET_TYPE_ALT})(?=\.|,|\s|$)",
|
rf"(?:{_DEDUP_STREET_TYPE_ALT})(?=\.|,|\s|$)",
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"high": {
|
"high": {
|
||||||
"coverage_pct": 75.0,
|
"coverage_pct": 75.0,
|
||||||
"mape_pct": 19.6,
|
"mape_pct": 19.97,
|
||||||
"n": 8,
|
"n": 8,
|
||||||
"n_covered": 6
|
"n_covered": 6
|
||||||
},
|
},
|
||||||
"low": {
|
"low": {
|
||||||
"coverage_pct": 83.59,
|
"coverage_pct": 83.67,
|
||||||
"mape_pct": 15.64,
|
"mape_pct": 15.64,
|
||||||
"n": 1562,
|
"n": 1562,
|
||||||
"n_covered": 1029
|
"n_covered": 1030
|
||||||
},
|
},
|
||||||
"medium": {
|
"medium": {
|
||||||
"coverage_pct": 70.0,
|
"coverage_pct": 70.0,
|
||||||
"mape_pct": 27.59,
|
"mape_pct": 26.53,
|
||||||
"n": 30,
|
"n": 30,
|
||||||
"n_covered": 21
|
"n_covered": 21
|
||||||
}
|
}
|
||||||
|
|
@ -26,17 +26,17 @@
|
||||||
],
|
],
|
||||||
"expected_sold": {
|
"expected_sold": {
|
||||||
"overall": {
|
"overall": {
|
||||||
"mape_pct": 15.88,
|
"mape_pct": 15.92,
|
||||||
"median_bias_pct": 2.87,
|
"median_bias_pct": 2.87,
|
||||||
"n": 1269,
|
"n": 1269,
|
||||||
"n_no_analogs": 0,
|
"n_no_analogs": 0,
|
||||||
"p25_pct": -10.88,
|
"p25_pct": -10.94,
|
||||||
"p75_pct": 22.98
|
"p75_pct": 22.81
|
||||||
},
|
},
|
||||||
"per_area_bucket": {
|
"per_area_bucket": {
|
||||||
"0 <30": {
|
"0 <30": {
|
||||||
"mape_pct": 19.76,
|
"mape_pct": 19.76,
|
||||||
"median_bias_pct": -1.49,
|
"median_bias_pct": -1.02,
|
||||||
"n": 161,
|
"n": 161,
|
||||||
"p25_pct": -16.45,
|
"p25_pct": -16.45,
|
||||||
"p75_pct": 34.7
|
"p75_pct": 34.7
|
||||||
|
|
@ -45,21 +45,21 @@
|
||||||
"mape_pct": 15.64,
|
"mape_pct": 15.64,
|
||||||
"median_bias_pct": 4.37,
|
"median_bias_pct": 4.37,
|
||||||
"n": 503,
|
"n": 503,
|
||||||
"p25_pct": -10.58,
|
"p25_pct": -10.56,
|
||||||
"p75_pct": 22.86
|
"p75_pct": 22.86
|
||||||
},
|
},
|
||||||
"2 44-62": {
|
"2 44-62": {
|
||||||
"mape_pct": 13.86,
|
"mape_pct": 13.93,
|
||||||
"median_bias_pct": 4.15,
|
"median_bias_pct": 4.13,
|
||||||
"n": 379,
|
"n": 379,
|
||||||
"p25_pct": -7.92,
|
"p25_pct": -8.12,
|
||||||
"p75_pct": 23.85
|
"p75_pct": 23.5
|
||||||
},
|
},
|
||||||
"3 62-85": {
|
"3 62-85": {
|
||||||
"mape_pct": 15.97,
|
"mape_pct": 15.97,
|
||||||
"median_bias_pct": -2.33,
|
"median_bias_pct": -2.33,
|
||||||
"n": 179,
|
"n": 179,
|
||||||
"p25_pct": -12.1,
|
"p25_pct": -11.99,
|
||||||
"p75_pct": 18.18
|
"p75_pct": 18.18
|
||||||
},
|
},
|
||||||
"4 >=85": {
|
"4 >=85": {
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
"0": {
|
"0": {
|
||||||
"label": "студия",
|
"label": "студия",
|
||||||
"mape_pct": 19.76,
|
"mape_pct": 19.76,
|
||||||
"median_bias_pct": -1.49,
|
"median_bias_pct": -1.02,
|
||||||
"n": 161,
|
"n": 161,
|
||||||
"n_no_analogs": 0,
|
"n_no_analogs": 0,
|
||||||
"p25_pct": -16.45,
|
"p25_pct": -16.45,
|
||||||
|
|
@ -86,17 +86,17 @@
|
||||||
"median_bias_pct": 4.37,
|
"median_bias_pct": 4.37,
|
||||||
"n": 503,
|
"n": 503,
|
||||||
"n_no_analogs": 0,
|
"n_no_analogs": 0,
|
||||||
"p25_pct": -10.58,
|
"p25_pct": -10.56,
|
||||||
"p75_pct": 22.86
|
"p75_pct": 22.86
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"label": "2к",
|
"label": "2к",
|
||||||
"mape_pct": 13.86,
|
"mape_pct": 13.93,
|
||||||
"median_bias_pct": 4.15,
|
"median_bias_pct": 4.13,
|
||||||
"n": 379,
|
"n": 379,
|
||||||
"n_no_analogs": 0,
|
"n_no_analogs": 0,
|
||||||
"p25_pct": -7.92,
|
"p25_pct": -8.12,
|
||||||
"p75_pct": 23.85
|
"p75_pct": 23.5
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"label": "3к",
|
"label": "3к",
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
"median_bias_pct": -2.33,
|
"median_bias_pct": -2.33,
|
||||||
"n": 179,
|
"n": 179,
|
||||||
"n_no_analogs": 0,
|
"n_no_analogs": 0,
|
||||||
"p25_pct": -12.1,
|
"p25_pct": -11.99,
|
||||||
"p75_pct": 18.18
|
"p75_pct": 18.18
|
||||||
},
|
},
|
||||||
"4": {
|
"4": {
|
||||||
|
|
@ -126,11 +126,11 @@
|
||||||
"p75_pct": -12.79
|
"p75_pct": -12.79
|
||||||
},
|
},
|
||||||
"комфорт": {
|
"комфорт": {
|
||||||
"mape_pct": 10.68,
|
"mape_pct": 10.69,
|
||||||
"median_bias_pct": -5.78,
|
"median_bias_pct": -5.78,
|
||||||
"n": 425,
|
"n": 425,
|
||||||
"p25_pct": -15.88,
|
"p25_pct": -15.98,
|
||||||
"p75_pct": 4.49
|
"p75_pct": 4.54
|
||||||
},
|
},
|
||||||
"премиум": {
|
"премиум": {
|
||||||
"mape_pct": null,
|
"mape_pct": null,
|
||||||
|
|
@ -140,11 +140,11 @@
|
||||||
"p75_pct": null
|
"p75_pct": null
|
||||||
},
|
},
|
||||||
"эконом": {
|
"эконом": {
|
||||||
"mape_pct": 20.17,
|
"mape_pct": 20.22,
|
||||||
"median_bias_pct": 18.98,
|
"median_bias_pct": 18.98,
|
||||||
"n": 697,
|
"n": 697,
|
||||||
"p25_pct": 1.25,
|
"p25_pct": 0.96,
|
||||||
"p75_pct": 41.97
|
"p75_pct": 42.31
|
||||||
},
|
},
|
||||||
"элит": {
|
"элит": {
|
||||||
"mape_pct": 36.11,
|
"mape_pct": 36.11,
|
||||||
|
|
@ -162,9 +162,9 @@
|
||||||
},
|
},
|
||||||
"range_coverage": {
|
"range_coverage": {
|
||||||
"overall": {
|
"overall": {
|
||||||
"coverage_pct": 83.22,
|
"coverage_pct": 83.29,
|
||||||
"n": 1269,
|
"n": 1269,
|
||||||
"n_covered": 1056
|
"n_covered": 1057
|
||||||
},
|
},
|
||||||
"per_confidence": {
|
"per_confidence": {
|
||||||
"high": {
|
"high": {
|
||||||
|
|
@ -173,9 +173,9 @@
|
||||||
"n_covered": 6
|
"n_covered": 6
|
||||||
},
|
},
|
||||||
"low": {
|
"low": {
|
||||||
"coverage_pct": 83.59,
|
"coverage_pct": 83.67,
|
||||||
"n": 1231,
|
"n": 1231,
|
||||||
"n_covered": 1029
|
"n_covered": 1030
|
||||||
},
|
},
|
||||||
"medium": {
|
"medium": {
|
||||||
"coverage_pct": 70.0,
|
"coverage_pct": 70.0,
|
||||||
|
|
|
||||||
|
|
@ -977,3 +977,70 @@ def test_parse_street_house_dangling_corpus_letter_no_digit_is_liter() -> None:
|
||||||
# сразу после буквы ⇒ литер, см. design §3), остаётся «65к» — НЕ
|
# сразу после буквы ⇒ литер, см. design §3), остаётся «65к» — НЕ
|
||||||
# схлопывается ни во что и не отбрасывается.
|
# схлопывается ни во что и не отбрасывается.
|
||||||
assert estimator._parse_street_house("ул. Ленина, 65к") == ("ленина", "65к")
|
assert estimator._parse_street_house("ул. Ленина, 65к") == ("ленина", "65к")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# #2291 класс 4 — нумерованные улицы с дефисом («40-летия Комсомола»)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("addr", "expected"),
|
||||||
|
[
|
||||||
|
# Живые форматы источников с прода (tradein-postgres, 2026-09-17).
|
||||||
|
("ул. 40-летия Комсомола,32Г", ("40-летия комсомола", "32г")), # avito
|
||||||
|
(
|
||||||
|
"Россия, Свердловская область, Екатеринбург, улица 40-летия Комсомола, 32Г",
|
||||||
|
("40-летия комсомола", "32г"),
|
||||||
|
), # yandex
|
||||||
|
("р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, 32Г", ("40-летия комсомола", "32г")),
|
||||||
|
("Екатеринбург, ЖБИ м-н, 40-летия Комсомола, 32Г", ("40-летия комсомола", "32г")),
|
||||||
|
("Москва, 60-летия Октября проспект, 18 к3", ("60-летия октября", "18/3")),
|
||||||
|
("улица 22-го Партсъезда, 10", ("22-го партсъезда", "10")),
|
||||||
|
# Регрессии: пробельная нумерованная, мкр-шум при уличном типе, bare-street, mkr.
|
||||||
|
("улица 8 Марта, 12", ("8 марта", "12")),
|
||||||
|
("мкр. ЖБИ, улица Сыромолотова, 5", ("сыромолотова", "5")),
|
||||||
|
("Заречный м-н, Готвальда, 24", ("готвальда", "24")),
|
||||||
|
("мкр Пионерский, 5", ("mkr пионерский", "5")),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_street_house_hyphen_numbered_street(addr: str, expected: tuple[str, str]) -> None:
|
||||||
|
assert estimator._parse_street_house(addr) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedup_collapses_hyphen_numbered_street_four_sources(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Один физлот в четырёх форматах: раньше avito/yandex давали ('', ''), cian —
|
||||||
|
# ('mkr жби', '40'), и слиться не мог никто.
|
||||||
|
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", True)
|
||||||
|
addrs = [
|
||||||
|
("avito", "ул. 40-летия Комсомола,32Г"),
|
||||||
|
("yandex", "Россия, Свердловская область, Екатеринбург, улица 40-летия Комсомола, 32Г"),
|
||||||
|
("cian", "р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, 32Г"),
|
||||||
|
("domklik", "Екатеринбург, ЖБИ м-н, 40-летия Комсомола, 32Г"),
|
||||||
|
]
|
||||||
|
lots = [
|
||||||
|
_lot(source=s, address=a, area=44.0, price=5_900_000, floor=7, cad=None) for s, a in addrs
|
||||||
|
]
|
||||||
|
assert len(estimator._dedup_cross_source(lots)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedup_hyphen_numbered_street_different_houses_stay_distinct(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# Раньше cian ключевал все дома улицы как ('mkr жби', '40'): дома 8А и 35 с
|
||||||
|
# одинаковыми этажом/площадью/ценой сливались в один лот.
|
||||||
|
monkeypatch.setattr(estimator.settings, "estimate_dedup_analogs_enabled", True)
|
||||||
|
lots = [
|
||||||
|
_lot(
|
||||||
|
source="cian",
|
||||||
|
address=f"р-н Кировский, мкр. ЖБИ, улица 40-летия Комсомола, {h}",
|
||||||
|
area=44.0,
|
||||||
|
price=5_900_000,
|
||||||
|
floor=7,
|
||||||
|
cad=None,
|
||||||
|
)
|
||||||
|
for h in ("8А", "35")
|
||||||
|
]
|
||||||
|
assert len(estimator._dedup_cross_source(lots)) == 2
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue