chore(tradein/backtest): --resolve-house-id flag to measure Tier-S + IMV anchor (#2002)
This commit is contained in:
parent
3187419eb8
commit
00e121056a
2 changed files with 185 additions and 17 deletions
|
|
@ -60,9 +60,12 @@ CAVEATS (read these before trusting the numbers)
|
||||||
network valuation sources (Avito-IMV on-demand eval, Yandex valuation,
|
network valuation sources (Avito-IMV on-demand eval, Yandex valuation,
|
||||||
Cian valuation) are NOT exercised (``imv_eval=None``,
|
Cian valuation) are NOT exercised (``imv_eval=None``,
|
||||||
``yandex/cian_val_present=False``). The populated `house_imv_evaluations`
|
``yandex/cian_val_present=False``). The populated `house_imv_evaluations`
|
||||||
anchor IS injected, but it keys on house_id which the harness does not
|
anchor IS injected, but it keys on house_id: by default the harness leaves
|
||||||
resolve (target_house_id=None) → in practice no IMV anchor fires. Real
|
``target_house_id=None`` → no IMV anchor fires. Pass ``--resolve-house-id``
|
||||||
estimate accuracy with the network layers may differ.
|
(#2002) to resolve each deal's canonical house via ``match_house_readonly``
|
||||||
|
so the Tier-S analog selector + the house IMV anchor fire, mirroring prod;
|
||||||
|
the resulting reach is reported in the ``house_id_resolution`` block. Real
|
||||||
|
estimate accuracy with the on-demand network layers may still differ.
|
||||||
(c) ДКП ≠ TRUE MARKET — a registered ДКП price is what the parties declared
|
(c) ДКП ≠ TRUE MARKET — a registered ДКП price is what the parties declared
|
||||||
to rosreestr; it can diverge from the genuine transaction price (tax
|
to rosreestr; it can diverge from the genuine transaction price (tax
|
||||||
optimisation, related-party sales, etc.).
|
optimisation, related-party sales, etc.).
|
||||||
|
|
@ -1054,17 +1057,83 @@ def _predict_for_deal(
|
||||||
return _percentile(prices, 0.5)
|
return _percentile(prices, 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _HouseIdResolution:
|
||||||
|
"""Coverage counters for ``--resolve-house-id`` (Tier-S + IMV anchor reach).
|
||||||
|
|
||||||
|
Populated only on the full-engine path when the flag is on. ``imv_reachable``
|
||||||
|
counts deals whose resolved house hit a band-compatible ``house_imv_evaluations``
|
||||||
|
row — i.e. ``_fetch_house_imv_anchor`` returned a non-None anchor that would
|
||||||
|
actually fire — so it measures the IMV anchor's true reach, not bare row
|
||||||
|
existence. Surfaced as the ``house_id_resolution`` block in ``--json``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
total: int = 0 # deals processed in the full-engine path under the flag
|
||||||
|
resolved: int = 0 # deals where match_house_readonly returned an id
|
||||||
|
imv_reachable: int = 0 # of resolved, those whose house_imv anchor fired
|
||||||
|
|
||||||
|
def as_json(self) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
"resolved": self.resolved,
|
||||||
|
"total": self.total,
|
||||||
|
"imv_reachable": self.imv_reachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_house_id_for_deal(db: Session, deal: DealSample) -> int | None:
|
||||||
|
"""Resolve a deal's canonical house_id (read-only) — mirrors prod (#2002).
|
||||||
|
|
||||||
|
Calls ``match_house_readonly(db, address=…, lat=…, lon=…)`` exactly as
|
||||||
|
``estimate_quality`` does (estimator.py:2554-2569): read-only, never INSERTs a
|
||||||
|
house, takes no advisory lock. Returns ``match[0]`` (the house_id), or ``None``
|
||||||
|
on no-match / empty-or-malformed tuple / any exception — best-effort, exactly
|
||||||
|
like prod which degrades to the geo tiers when no house resolves.
|
||||||
|
|
||||||
|
A defensive ``db.rollback()`` runs if the lookup raises so a poisoned tx does
|
||||||
|
not bleed into the per-deal spine queries (consistent with the loop's own
|
||||||
|
rollback-on-failure handling).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.services.matching.houses import ( # type: ignore[import-not-found]
|
||||||
|
match_house_readonly,
|
||||||
|
)
|
||||||
|
except ImportError: # pragma: no cover — fallback for adhoc invocation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
from app.services.matching.houses import match_house_readonly
|
||||||
|
|
||||||
|
try:
|
||||||
|
match = match_house_readonly(db, address=deal.address, lat=deal.lat, lon=deal.lon)
|
||||||
|
except Exception as exc: # pragma: no cover — defensive, mirrors prod best-effort
|
||||||
|
logger.warning("deal %s house_id resolution failed (graceful): %s", deal.id, exc)
|
||||||
|
db.rollback()
|
||||||
|
return None
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(match[0])
|
||||||
|
except (TypeError, ValueError, IndexError): # pragma: no cover — malformed return
|
||||||
|
logger.warning("deal %s house match returned malformed tuple: %r", deal.id, match)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _select_analogs_full(
|
def _select_analogs_full(
|
||||||
db: Session, deal: DealSample, est: SimpleNamespace
|
db: Session,
|
||||||
|
deal: DealSample,
|
||||||
|
est: SimpleNamespace,
|
||||||
|
*,
|
||||||
|
target_house_id: int | None = None,
|
||||||
) -> tuple[list[dict[str, Any]], str, bool, bool]:
|
) -> tuple[list[dict[str, Any]], str, bool, bool]:
|
||||||
"""Replicate estimate_quality's analog tier ladder for one deal (#1966).
|
"""Replicate estimate_quality's analog tier ladder for one deal (#1966).
|
||||||
|
|
||||||
Mirrors the Tier0(cohort)→A(1 km)→wide(2 km)→widearea(±25 %) sequence
|
Mirrors the Tier0(cohort)→A(1 km)→wide(2 km)→widearea(±25 %) sequence
|
||||||
EXACTLY, including the MIN_ANALOGS_TIER_0 gate and the <5 / <3 widen
|
EXACTLY, including the MIN_ANALOGS_TIER_0 gate and the <5 / <3 widen
|
||||||
thresholds and the "only adopt the wider tier when it returns MORE lots"
|
thresholds and the "only adopt the wider tier when it returns MORE lots"
|
||||||
guards. target_house_id is always None here (the harness does not resolve
|
guards. ``target_house_id`` defaults to None — same as a fresh estimate
|
||||||
canonical houses), so Tier S(canonical) never fires — same as a fresh
|
without a house match, so Tier S(canonical) never fires. When the harness is
|
||||||
estimate without a house match.
|
run with ``--resolve-house-id`` (#2002) a resolved id is threaded in and the
|
||||||
|
Tier-S ``WHERE house_id_fk = :target_house_id`` selector fires, mirroring prod.
|
||||||
|
|
||||||
Returns ``(listings, analog_tier, fallback_used, area_widened)``.
|
Returns ``(listings, analog_tier, fallback_used, area_widened)``.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1080,7 +1149,7 @@ def _select_analogs_full(
|
||||||
area=deal.area_m2,
|
area=deal.area_m2,
|
||||||
radius_m=m.DEFAULT_RADIUS_M,
|
radius_m=m.DEFAULT_RADIUS_M,
|
||||||
full_address=deal.address,
|
full_address=deal.address,
|
||||||
target_house_id=None,
|
target_house_id=target_house_id,
|
||||||
year_built=deal.year_built,
|
year_built=deal.year_built,
|
||||||
house_type=deal.house_type,
|
house_type=deal.house_type,
|
||||||
total_floors=deal.total_floors,
|
total_floors=deal.total_floors,
|
||||||
|
|
@ -1103,7 +1172,7 @@ def _select_analogs_full(
|
||||||
area=deal.area_m2,
|
area=deal.area_m2,
|
||||||
radius_m=m.DEFAULT_RADIUS_M,
|
radius_m=m.DEFAULT_RADIUS_M,
|
||||||
full_address=deal.address,
|
full_address=deal.address,
|
||||||
target_house_id=None,
|
target_house_id=target_house_id,
|
||||||
year_built=deal.year_built,
|
year_built=deal.year_built,
|
||||||
house_type=deal.house_type,
|
house_type=deal.house_type,
|
||||||
total_floors=deal.total_floors,
|
total_floors=deal.total_floors,
|
||||||
|
|
@ -1119,7 +1188,7 @@ def _select_analogs_full(
|
||||||
area=deal.area_m2,
|
area=deal.area_m2,
|
||||||
radius_m=m.FALLBACK_RADIUS_M,
|
radius_m=m.FALLBACK_RADIUS_M,
|
||||||
full_address=deal.address,
|
full_address=deal.address,
|
||||||
target_house_id=None,
|
target_house_id=target_house_id,
|
||||||
year_built=deal.year_built,
|
year_built=deal.year_built,
|
||||||
house_type=deal.house_type,
|
house_type=deal.house_type,
|
||||||
total_floors=deal.total_floors,
|
total_floors=deal.total_floors,
|
||||||
|
|
@ -1139,7 +1208,7 @@ def _select_analogs_full(
|
||||||
radius_m=m.FALLBACK_RADIUS_M,
|
radius_m=m.FALLBACK_RADIUS_M,
|
||||||
area_tolerance=0.25,
|
area_tolerance=0.25,
|
||||||
full_address=deal.address,
|
full_address=deal.address,
|
||||||
target_house_id=None,
|
target_house_id=target_house_id,
|
||||||
year_built=deal.year_built,
|
year_built=deal.year_built,
|
||||||
house_type=deal.house_type,
|
house_type=deal.house_type,
|
||||||
total_floors=deal.total_floors,
|
total_floors=deal.total_floors,
|
||||||
|
|
@ -1345,6 +1414,8 @@ def _predict_full_spine(
|
||||||
est: SimpleNamespace,
|
est: SimpleNamespace,
|
||||||
*,
|
*,
|
||||||
capture: list[dict[str, Any]] | None = None,
|
capture: list[dict[str, Any]] | None = None,
|
||||||
|
target_house_id: int | None = None,
|
||||||
|
resolution: _HouseIdResolution | None = None,
|
||||||
) -> Prediction | None:
|
) -> Prediction | None:
|
||||||
"""Predict one deal through the FULL deterministic spine (#1966).
|
"""Predict one deal through the FULL deterministic spine (#1966).
|
||||||
|
|
||||||
|
|
@ -1362,11 +1433,20 @@ def _predict_full_spine(
|
||||||
When ``capture`` is a list, every deal that yields a prediction also appends a
|
When ``capture`` is a list, every deal that yields a prediction also appends a
|
||||||
fully JSON-plain replay record (frozen kwargs + the 3 callables' recorded
|
fully JSON-plain replay record (frozen kwargs + the 3 callables' recorded
|
||||||
``[arg -> return]`` call-lists) — see ``--dump-fixture`` / ``replay_fixture``.
|
``[arg -> return]`` call-lists) — see ``--dump-fixture`` / ``replay_fixture``.
|
||||||
|
|
||||||
|
``target_house_id`` (#2002) is None by default — the historical harness
|
||||||
|
behaviour, where Tier-S(canonical) and the house Avito-IMV anchor never fire.
|
||||||
|
When ``--resolve-house-id`` is on the caller resolves it and threads it here so
|
||||||
|
those paths become measurable; ``resolution`` (when provided) is the coverage
|
||||||
|
accumulator whose ``imv_reachable`` counter this function bumps once it knows
|
||||||
|
whether the house IMV anchor actually resolved a row for this deal.
|
||||||
"""
|
"""
|
||||||
m = est.m
|
m = est.m
|
||||||
settings = est.settings
|
settings = est.settings
|
||||||
|
|
||||||
listings, analog_tier, fallback_used, area_widened = _select_analogs_full(db, deal, est)
|
listings, analog_tier, fallback_used, area_widened = _select_analogs_full(
|
||||||
|
db, deal, est, target_house_id=target_house_id
|
||||||
|
)
|
||||||
|
|
||||||
# ── Pre-fetch the spine inputs (same calls estimate_quality hoists) ───────
|
# ── Pre-fetch the spine inputs (same calls estimate_quality hoists) ───────
|
||||||
dkp_raw = m._fetch_dkp_corridor(db, address=deal.address, rooms=deal.rooms, area=deal.area_m2)
|
dkp_raw = m._fetch_dkp_corridor(db, address=deal.address, rooms=deal.rooms, area=deal.area_m2)
|
||||||
|
|
@ -1374,6 +1454,10 @@ def _predict_full_spine(
|
||||||
# estimate_quality (estimator.py L2862-2881) — disabled / no-area / no-address
|
# estimate_quality (estimator.py L2862-2881) — disabled / no-area / no-address
|
||||||
# → ([], None) instead of an unconditional fetch.
|
# → ([], None) instead of an unconditional fetch.
|
||||||
if settings.estimate_same_building_anchor_enabled and deal.area_m2 and deal.address:
|
if settings.estimate_same_building_anchor_enabled and deal.area_m2 and deal.address:
|
||||||
|
# NB(#2002): kept at None deliberately — the deliverable scopes the
|
||||||
|
# resolved-id threading to the Tier-S analog ladder + the house IMV anchor.
|
||||||
|
# Prod ALSO resolves this same-building anchor (estimator.py:2782); flip to
|
||||||
|
# `target_house_id=target_house_id` here if/when full prod parity is wanted.
|
||||||
anchor_comps, anchor_tier = m._fetch_anchor_comps(
|
anchor_comps, anchor_tier = m._fetch_anchor_comps(
|
||||||
db,
|
db,
|
||||||
address=deal.address,
|
address=deal.address,
|
||||||
|
|
@ -1386,8 +1470,13 @@ def _predict_full_spine(
|
||||||
else:
|
else:
|
||||||
anchor_comps, anchor_tier = [], None
|
anchor_comps, anchor_tier = [], None
|
||||||
imv_anchor = m._fetch_house_imv_anchor(
|
imv_anchor = m._fetch_house_imv_anchor(
|
||||||
db, target_house_id=None, rooms=deal.rooms, area=deal.area_m2
|
db, target_house_id=target_house_id, rooms=deal.rooms, area=deal.area_m2
|
||||||
)
|
)
|
||||||
|
# #2002: count IMV reach — a resolved house whose house_imv_evaluations row
|
||||||
|
# actually fires the anchor (band-compatible match → non-None). Counted before
|
||||||
|
# the median<=0 skip below so reach reflects row availability, not pricing.
|
||||||
|
if resolution is not None and target_house_id is not None and imv_anchor is not None:
|
||||||
|
resolution.imv_reachable += 1
|
||||||
|
|
||||||
# Synthetic geo = the deal's own coords + address, confidence "exact" — the
|
# Synthetic geo = the deal's own coords + address, confidence "exact" — the
|
||||||
# faithful analog of estimate_quality's client-coords fast path. Real deal
|
# faithful analog of estimate_quality's client-coords fast path. Real deal
|
||||||
|
|
@ -1658,7 +1747,12 @@ def run_backtest(
|
||||||
|
|
||||||
|
|
||||||
def run_backtest_full(
|
def run_backtest_full(
|
||||||
db: Session, *, sample: int, since: str, dump_fixture: str | None = None
|
db: Session,
|
||||||
|
*,
|
||||||
|
sample: int,
|
||||||
|
since: str,
|
||||||
|
dump_fixture: str | None = None,
|
||||||
|
resolve_house_id: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Drive the FULL-spine read-only backtest and return a metrics dict (#1966).
|
"""Drive the FULL-spine read-only backtest and return a metrics dict (#1966).
|
||||||
|
|
||||||
|
|
@ -1677,6 +1771,12 @@ def run_backtest_full(
|
||||||
the 3 DB callables' recorded call-lists are frozen into a committed JSON file
|
the 3 DB callables' recorded call-lists are frozen into a committed JSON file
|
||||||
(schema_version=1) so ``replay_fixture`` can re-score offline against a frozen
|
(schema_version=1) so ``replay_fixture`` can re-score offline against a frozen
|
||||||
baseline (the CI regression gate). This is the only path that writes a file.
|
baseline (the CI regression gate). This is the only path that writes a file.
|
||||||
|
|
||||||
|
When ``resolve_house_id`` is True (#2002, ``--resolve-house-id``) each deal's
|
||||||
|
canonical house_id is resolved per ``match_house_readonly`` and threaded into
|
||||||
|
the Tier-S analog ladder + the house Avito-IMV anchor, mirroring prod, and a
|
||||||
|
``house_id_resolution`` coverage block (resolved / total / imv_reachable) is
|
||||||
|
attached to the returned metrics. Default False → byte-identical prior output.
|
||||||
"""
|
"""
|
||||||
est = _import_estimator_full()
|
est = _import_estimator_full()
|
||||||
deals = _load_sample(db, sample=sample, since=since)
|
deals = _load_sample(db, sample=sample, since=since)
|
||||||
|
|
@ -1689,10 +1789,24 @@ def run_backtest_full(
|
||||||
sold_ppm2_all: list[float] = [d.sold_ppm2 for d in deals]
|
sold_ppm2_all: list[float] = [d.sold_ppm2 for d in deals]
|
||||||
pred_ppm2_all: list[float] = [] # asking headline median_ppm2 (priced deals)
|
pred_ppm2_all: list[float] = [] # asking headline median_ppm2 (priced deals)
|
||||||
capture: list[dict[str, Any]] | None = [] if dump_fixture else None
|
capture: list[dict[str, Any]] | None = [] if dump_fixture else None
|
||||||
|
resolution: _HouseIdResolution | None = _HouseIdResolution() if resolve_house_id else None
|
||||||
|
|
||||||
for i, deal in enumerate(deals, start=1):
|
for i, deal in enumerate(deals, start=1):
|
||||||
|
target_house_id: int | None = None
|
||||||
|
if resolution is not None:
|
||||||
|
resolution.total += 1
|
||||||
|
target_house_id = _resolve_house_id_for_deal(db, deal)
|
||||||
|
if target_house_id is not None:
|
||||||
|
resolution.resolved += 1
|
||||||
try:
|
try:
|
||||||
pr = _predict_full_spine(db, deal, est, capture=capture)
|
pr = _predict_full_spine(
|
||||||
|
db,
|
||||||
|
deal,
|
||||||
|
est,
|
||||||
|
capture=capture,
|
||||||
|
target_house_id=target_house_id,
|
||||||
|
resolution=resolution,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Read-only: a failed SELECT can poison the tx → rollback so the next
|
# Read-only: a failed SELECT can poison the tx → rollback so the next
|
||||||
# deal's queries run clean. Skip this deal (counts as no-prediction).
|
# deal's queries run clean. Skip this deal (counts as no-prediction).
|
||||||
|
|
@ -1743,6 +1857,17 @@ def run_backtest_full(
|
||||||
"price_segments_ppm2": [list(seg) for seg in PRICE_SEGMENTS_PPM2],
|
"price_segments_ppm2": [list(seg) for seg in PRICE_SEGMENTS_PPM2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# #2002: house_id resolution coverage — the key Tier-S + IMV reach number.
|
||||||
|
# Only emitted under --resolve-house-id so the default output stays unchanged.
|
||||||
|
if resolution is not None:
|
||||||
|
metrics["house_id_resolution"] = resolution.as_json()
|
||||||
|
logger.info(
|
||||||
|
"house_id resolution: resolved=%d/%d imv_reachable=%d",
|
||||||
|
resolution.resolved,
|
||||||
|
resolution.total,
|
||||||
|
resolution.imv_reachable,
|
||||||
|
)
|
||||||
|
|
||||||
if dump_fixture is not None and capture is not None:
|
if dump_fixture is not None and capture is not None:
|
||||||
_write_fixture(dump_fixture, capture=capture, since=since, est=est)
|
_write_fixture(dump_fixture, capture=capture, since=since, est=est)
|
||||||
|
|
||||||
|
|
@ -1844,6 +1969,18 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Emit machine-readable JSON instead of the text table.",
|
help="Emit machine-readable JSON instead of the text table.",
|
||||||
)
|
)
|
||||||
|
p.add_argument(
|
||||||
|
"--resolve-house-id",
|
||||||
|
action="store_true",
|
||||||
|
help="FULL engine only: resolve each deal's canonical house_id via "
|
||||||
|
"match_house_readonly(address, lat, lon) and thread it into the analog "
|
||||||
|
"tier ladder + house Avito-IMV anchor, mirroring prod estimate_quality "
|
||||||
|
"(estimator.py:2554-2569). Lets the Tier-S 'same-building' selector and "
|
||||||
|
"_fetch_house_imv_anchor actually fire (they key on house_id, otherwise "
|
||||||
|
"None → never resolved). Adds a 'house_id_resolution' coverage block to "
|
||||||
|
"the JSON output. Default OFF → byte-identical to the prior behaviour, so "
|
||||||
|
"the frozen regression gate is untouched.",
|
||||||
|
)
|
||||||
# #1966 PR 3/3 — fixture capture + hermetic replay. --dump-fixture (DB run,
|
# #1966 PR 3/3 — fixture capture + hermetic replay. --dump-fixture (DB run,
|
||||||
# full engine) and --from-fixture (NO DB) are mutually exclusive modes.
|
# full engine) and --from-fixture (NO DB) are mutually exclusive modes.
|
||||||
fixture_mode = p.add_mutually_exclusive_group()
|
fixture_mode = p.add_mutually_exclusive_group()
|
||||||
|
|
@ -1895,10 +2032,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
raise SystemExit("--update-baseline requires --from-fixture")
|
raise SystemExit("--update-baseline requires --from-fixture")
|
||||||
if args.dump_fixture and args.engine != "full":
|
if args.dump_fixture and args.engine != "full":
|
||||||
raise SystemExit("--dump-fixture is only supported with --engine full")
|
raise SystemExit("--dump-fixture is only supported with --engine full")
|
||||||
|
if args.resolve_house_id and args.engine != "full":
|
||||||
|
raise SystemExit("--resolve-house-id is only supported with --engine full")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"backtest start: engine=%s sample=%d since=%s radius=%dm "
|
"backtest start: engine=%s sample=%d since=%s radius=%dm "
|
||||||
"rooms_tolerance=%d holdout_split=%s dump_fixture=%s",
|
"rooms_tolerance=%d holdout_split=%s dump_fixture=%s resolve_house_id=%s",
|
||||||
args.engine,
|
args.engine,
|
||||||
args.sample,
|
args.sample,
|
||||||
args.since,
|
args.since,
|
||||||
|
|
@ -1906,13 +2045,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
args.rooms_tolerance,
|
args.rooms_tolerance,
|
||||||
args.holdout_split,
|
args.holdout_split,
|
||||||
args.dump_fixture,
|
args.dump_fixture,
|
||||||
|
args.resolve_house_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
db = _session()
|
db = _session()
|
||||||
try:
|
try:
|
||||||
if args.engine == "full":
|
if args.engine == "full":
|
||||||
metrics = run_backtest_full(
|
metrics = run_backtest_full(
|
||||||
db, sample=args.sample, since=args.since, dump_fixture=args.dump_fixture
|
db,
|
||||||
|
sample=args.sample,
|
||||||
|
since=args.since,
|
||||||
|
dump_fixture=args.dump_fixture,
|
||||||
|
resolve_house_id=args.resolve_house_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
metrics = run_backtest(
|
metrics = run_backtest(
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,30 @@ def test_argparse_engine_rejects_unknown() -> None:
|
||||||
bt._parse_args(["--engine", "bogus"])
|
bt._parse_args(["--engine", "bogus"])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# #2002 --resolve-house-id flag + its coverage accumulator.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_argparse_resolve_house_id_defaults_off() -> None:
|
||||||
|
# Default OFF keeps the harness byte-identical so the regression gate stands.
|
||||||
|
assert bt._parse_args([]).resolve_house_id is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_argparse_resolve_house_id_override() -> None:
|
||||||
|
assert bt._parse_args(["--resolve-house-id"]).resolve_house_id is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_house_id_resolution_as_json_shape() -> None:
|
||||||
|
res = bt._HouseIdResolution(total=10, resolved=4, imv_reachable=2)
|
||||||
|
assert res.as_json() == {"resolved": 4, "total": 10, "imv_reachable": 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_house_id_resolution_defaults_zero() -> None:
|
||||||
|
res = bt._HouseIdResolution()
|
||||||
|
assert res.as_json() == {"resolved": 0, "total": 0, "imv_reachable": 0}
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# #1966 _bucketize_confidence / _segment_label — pure bucketing.
|
# #1966 _bucketize_confidence / _segment_label — pure bucketing.
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue