Commit graph

7 commits

Author SHA1 Message Date
e4975c486f fix(forecasting): correct _geo_weight decay/floor so far projects get low weight (#1633)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / backend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m40s
CI / backend-tests (pull_request) Failing after 8m48s
_GEO_WEIGHT_UNKNOWN was 0.1, which equals exp(−6.9/3)≈0.10 (weight of a
confirmed-far project at ~6.9 km). Projects beyond that distance got a
weight *below* 0.1, meaning unknown-coordinate projects outweighed
confirmed-far ones — an inversion of the documented intent.

Lowered to 0.05 (≈ exp(−3) = exp(−9 km / scale)), restoring the correct
hierarchy: confirmed-close > confirmed-far > unknown. Updated TestGeoWeight
(hardcoded 0.1 expectation) and TestCannibalizationTrueMode (overlap ×
floor comment/value) accordingly. Added two new assertions in TestGeoWeight
that enforce the hierarchy monotonically and verify unknown < exp(−6.9/3).
2026-06-17 20:44:00 +03:00
10df430cac fix(forecasting): _price_overlap zero-width discontinuity at §25.3 (#1224)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Degenerate price band (own_min==own_max или c_lo==c_hi, оба разрешены
CHECK 148 и Pydantic) внутри другой вилки возвращали 0.0 вместо 1.0:
фильтр w>0 выкидывал нулевую ширину → 0/positive=0.0. Это рвало
докстринг 'полное накрытие узкого = 1.0' и давало разрыв:
[148k,152k]→1.0 vs [150k,150k]→0.0, занижая среднее каннибализации.

Patch: вырожденные ширины обрабатываются ДО нормирования.
lo<=hi → точка внутри другой вилки → 1.0, вне → 0.0. +inf-обе-премиум
ветка перенесена в начало (избежать inf-inf=nan). +7 новых тестов в
TestPriceOverlap. 220 special_indices тестов зелёные.

Closes #1224
2026-06-13 08:10:46 +00:00
c48f45c48f fix(forecasting): resolve admin district to micros в _query_artificial_demand (#1205)
Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
objective_lots.district хранит МИКРО-вокабуляр ('Уралмаш', 'ЖБИ', ...).
_query_artificial_demand фильтровал сырым АДМИН-именем ('Кировский' с
forecast.py:123) → ol.district='Кировский' = 0 строк → n_sold=0 → §25.5
Artificial Demand 'unavailable' с ложной причиной «нет проданных лотов»
в каждом district-scoped отчёте. Тот же класс бага, что #1211 в
_price_sensitivity.

Patch: импорт resolve_objective_districts + замена сырого
`ol.district = CAST(:district AS text)` на зеркальный паттерн
sales_series._SOURCE_B_SQL / market_metrics._SALES_WINDOW_SQL:
  (CAST(:has_district AS boolean) IS FALSE
   OR ol.district = ANY(CAST(:districts AS text[])))

Сигнатура _query_artificial_demand / _build_artificial_demand НЕ меняется
— caller остаётся admin-aware на входе.

+5 новых тестов (TestArtificialDemandDistrictResolution: резолвер вызван,
микро в bind, n_sold>0 после фикса), 6 обновлённых SQL-тестов. 21 passed
artificial_demand + 1030 forecasting тестов зелёные. ruff clean.

Closes #1205
2026-06-13 07:34:54 +00:00
c9720a6212 feat(forecasting): activate §25.3 cannibalization unit-mix axis (4/4), gated (#1169)
All checks were successful
Deploy / build-worker (push) Successful in 3m1s
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 5s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m32s
CI / backend-tests (pull_request) Successful in 6m24s
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m1s
Deploy / deploy (push) Successful in 1m25s
Feed candidate_unit_mix into _build_cannibalization (mirrors how #1169 fed
candidate_release_month from the launch window), completing §25.3 to all 4 axes:
class + price + timing + unit-mix, plus geo weight.

- candidate mix from recommend_mix "buckets[].share_pct" (same rule-based
  квартирография as §22 product_tz), extracted + normalized to {bucket: share}.
- _canonical_room_bucket folds recommend_mix RU labels ("1-к 30-45", "80+ м²")
  and manual own_planned_project Latin keys ("1k") into one room-count space —
  without it the L1 similarity would silently be 0 (disjoint keys).
- recommend_mix is HEAVY, so it's GATED: derived only when the own-portfolio has
  >=1 project with a non-empty unit_mix; get_own_portfolio fetched once in
  compute_special_indices and threaded into _build_cannibalization (no double
  fetch). With OWN_DEVELOPER_IDS unset (portfolio empty) → zero added cost on the
  hot §22 report path.
- Graceful (recommend_mix None/empty/raises → axis excluded, None-not-0),
  deterministic. Unit-mix only fires for manual-future own-projects with a mix
  (domrf-current carry unit_mix=None) — expected narrowness, documented.

205 tests; ruff + mypy clean. Scope: special_indices.py + test only; no deps.

Refs #1169
2026-06-09 16:09:29 +05:00
cd48a095c0 feat(forecasting): activate §25.3 cannibalization timing axis from launch window (#1169)
Some checks failed
Deploy / build-worker (push) Successful in 2m32s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m32s
Deploy / deploy (push) Has been cancelled
Derive candidate_release_month = report-as-of (date.today()) + §25.1 Launch
Window peak-deficit horizon, threaded into _build_cannibalization so the timing
overlap axis activates against own-portfolio release_month (near-in-time own
projects raise cannibalization risk). Launch Window now computed once in
compute_special_indices and reused (no double-compute). Launch Window
unavailable -> candidate_release_month None -> timing axis gracefully excluded
(None-not-0); cannibalization still scores on class/price/geo. Adds stdlib
_add_months helper (year-boundary safe, no new dep). Deterministic. 168 tests.

§25.3 now: class+price+timing+geo active; unit-mix remains phase-2.

Refs #1169
2026-06-09 07:10:59 +00:00
347203dfda feat(forecasting): §25.3 TRUE own-portfolio cannibalization overlap (#1169 PR2)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Successful in 6m26s
CI / backend-tests (pull_request) Successful in 6m21s
Deploy / changes (push) Successful in 6s
Deploy / build-worker (push) Successful in 3m10s
Deploy / deploy (push) Successful in 1m41s
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m47s
Upgrade the §25.3 cannibalization index from a same-class-competitor proxy to
true own-portfolio overlap: score the recommended candidate segment against the
developer's own portfolio (get_own_portfolio, #1169 PR1) across axes —
audience/class (ordinal distance), price ₽/м² (interval overlap), unit-mix
(L1 similarity), timing (half-life decay) — geo-weighted by haversine proximity
to the parcel, aggregated by geo-weighted soft-max (the strongest nearby
cannibalizer dominates, not a diluting mean). Empty portfolio -> labelled proxy
fallback, confidence forced low, never presented as the true index.

Pure scoring fns unit-tested without DB; None-not-0 on missing axes; thin/
only-current portfolio -> low confidence + §26 note; deterministic (sorted
tie-breaks, no RNG). class+price+geo active now; unit-mix+timing plumbed via
optional params for a follow-up that wires them from the orchestrator horizon.
ruff + mypy clean; 151 special-index tests pass (964 forecasting dir, no regr).

Refs #1169
2026-06-08 16:47:18 +05:00
008f9d9ed1 feat(forecasting): §25 six special indices (#986, 954-C) (#1018)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m28s
Deploy / build-worker (push) Successful in 2m31s
Deploy / deploy (push) Successful in 1m8s
2026-06-03 08:24:08 +00:00