feat(forecasting): §25.3 TRUE own-portfolio cannibalization overlap (#1169 PR2) #1171

Merged
bot-backend merged 1 commit from feat/cannibalization-overlap-952b into main 2026-06-08 11:58:03 +00:00
Collaborator

Summary

PR 2 (Integration) of #1169 — §25.3 cannibalization upgraded from a proxy (same-class competitor density, #986) to TRUE own-portfolio overlap.

_build_cannibalization now calls get_own_portfolio(db) (PR1):

  • non-empty → score the recommended candidate segment vs each own project across axes — class (ordinal distance, case/lang-insensitive via competitors._normalize_class), price ₽/м² (interval overlap on the narrower band), unit-mix (1 − ½·L1), timing (half-life decay, 0.5 @ 12mo) — weighted by geo proximity (haversine + exp decay to the parcel), aggregated by geo-weighted soft-max (a single strong nearby cannibalizer dominates, not a diluting mean). Explanation lists top contributing own projects + per-axis breakdown (§16 explainability).
  • empty (own-portfolio not configured) → labelled proxy fallback, confidence forced low, proxy=true + reason — never presented as the true index.

Honest confidence (§26): thin / only-current (no future pipeline) portfolio → low + note. None-not-0 on every missing axis. Deterministic (sorted tie-breaks, no RNG). advisory=True always, confidence ≤ cap.

Axis status: class + price + geo active now. unit-mix + timing are plumbed via optional params but not yet fed by the call site — a small follow-up will wire candidate_release_month from the orchestrator horizon (special_indices.py call site) to activate timing; unit-mix needs a project-level recommended mix (phase 2).

Test plan

  • uv run pytest tests/services/forecasting/test_special_indices.py -q151 passed (964 forecasting dir, no regressions)
  • ruff check + mypy special_indices.py → clean (0 errors in file)
  • Tests: each axis (class match/adjacent/far, price interval, unit-mix, timing, geo), soft-max aggregation, empty→proxy-fallback (labelled), non-empty→true, None-not-0, thin-portfolio low-confidence, determinism, tie-break
  • code-reviewer APPROVE (all 3 blocker classes cleared: no fabricated-0, proxy never mistaken for true, deterministic; math bounded [0,1])
  • needs-config: activates only once OWN_DEVELOPER_IDS is set (chip filed) — until then runs the labelled proxy fallback.

Follow-up

  • PR 3 (small): wire candidate_release_month from orchestrator horizon → timing axis.

Refs #1169

## Summary PR 2 (Integration) of #1169 — §25.3 cannibalization upgraded from a **proxy** (same-class competitor density, #986) to **TRUE own-portfolio overlap**. `_build_cannibalization` now calls `get_own_portfolio(db)` (PR1): - **non-empty** → score the recommended candidate segment vs each own project across axes — **class** (ordinal distance, case/lang-insensitive via `competitors._normalize_class`), **price ₽/м²** (interval overlap on the narrower band), **unit-mix** (1 − ½·L1), **timing** (half-life decay, 0.5 @ 12mo) — weighted by **geo** proximity (haversine + exp decay to the parcel), aggregated by **geo-weighted soft-max** (a single strong nearby cannibalizer dominates, not a diluting mean). Explanation lists top contributing own projects + per-axis breakdown (§16 explainability). - **empty** (own-portfolio not configured) → labelled **proxy fallback**, confidence forced low, `proxy=true` + reason — never presented as the true index. Honest confidence (§26): thin / only-current (no future pipeline) portfolio → low + note. None-not-0 on every missing axis. Deterministic (sorted tie-breaks, no RNG). advisory=True always, confidence ≤ cap. **Axis status:** class + price + geo active now. unit-mix + timing are plumbed via optional params but not yet fed by the call site — a small follow-up will wire `candidate_release_month` from the orchestrator horizon (`special_indices.py` call site) to activate timing; unit-mix needs a project-level recommended mix (phase 2). ## Test plan - [x] `uv run pytest tests/services/forecasting/test_special_indices.py -q` → **151 passed** (964 forecasting dir, no regressions) - [x] `ruff check` + `mypy special_indices.py` → clean (0 errors in file) - [x] Tests: each axis (class match/adjacent/far, price interval, unit-mix, timing, geo), soft-max aggregation, empty→proxy-fallback (labelled), non-empty→true, None-not-0, thin-portfolio low-confidence, determinism, tie-break - [x] code-reviewer ✅ APPROVE (all 3 blocker classes cleared: no fabricated-0, proxy never mistaken for true, deterministic; math bounded [0,1]) - [ ] needs-config: activates only once `OWN_DEVELOPER_IDS` is set (chip filed) — until then runs the labelled proxy fallback. ## Follow-up - PR 3 (small): wire `candidate_release_month` from orchestrator horizon → timing axis. Refs #1169
bot-backend added 1 commit 2026-06-08 11:47:45 +00:00
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
347203dfda
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
bot-backend merged commit 347203dfda into main 2026-06-08 11:58:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1171
No description provided.