fix(forecasting): wire deal_count_months from market window into confidence note (#1637)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m39s
CI / backend-tests (pull_request) Has been cancelled
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m39s
CI / backend-tests (pull_request) Has been cancelled
`_build_confidence` was calling `compute_report_confidence` without `deal_count_months`, so the «за N мес» suffix never appeared in production. Add `_deal_count_months(market_metrics)` extractor (reads `window_months`, same key as `_history_months`) and pass it. Also fix pre-existing UP038 violations (isinstance tuple → X | Y).
This commit is contained in:
parent
afa7d80ecf
commit
ac47cd73a6
1 changed files with 23 additions and 9 deletions
|
|
@ -189,10 +189,10 @@ def _domrf_coverage(analyze: dict[str, Any], supply_layers: dict[str, Any] | Non
|
|||
"""
|
||||
if supply_layers is not None:
|
||||
coverage = supply_layers.get("domrf_coverage")
|
||||
if isinstance(coverage, (int, float)) and not isinstance(coverage, bool):
|
||||
if isinstance(coverage, int | float) and not isinstance(coverage, bool):
|
||||
return _clamp_fraction(float(coverage))
|
||||
pct = analyze.get("market_data_coverage_pct")
|
||||
if isinstance(pct, (int, float)) and not isinstance(pct, bool):
|
||||
if isinstance(pct, int | float) and not isinstance(pct, bool):
|
||||
return _clamp_fraction(float(pct) / 100.0)
|
||||
return None
|
||||
|
||||
|
|
@ -223,6 +223,19 @@ def _history_months(
|
|||
return None
|
||||
|
||||
|
||||
def _deal_count_months(market_metrics: dict[str, Any] | None) -> int | None:
|
||||
"""Окно наблюдения для deal_count (мес) — для deal_count_months #990. PURE.
|
||||
|
||||
Читает тот же `market_metrics.window_months` (§9.2), что и `_history_months` —
|
||||
именно за это окно считается n_sold. Нет → None (#990 пропускает суффикс «за N мес»).
|
||||
"""
|
||||
if market_metrics is not None:
|
||||
window = market_metrics.get("window_months")
|
||||
if isinstance(window, int) and window > 0:
|
||||
return window
|
||||
return None
|
||||
|
||||
|
||||
def _confounded(forecasts: Sequence[dict[str, Any]]) -> bool:
|
||||
"""Пересекает ли окно прогноза шок-период — для confounded #990. PURE.
|
||||
|
||||
|
|
@ -293,10 +306,10 @@ def _primary_deficit_index(forecasts: Sequence[dict[str, Any]]) -> float | None:
|
|||
)
|
||||
if primary is not None and primary.get("deficit_index") is not None:
|
||||
di = primary["deficit_index"]
|
||||
return float(di) if isinstance(di, (int, float)) and not isinstance(di, bool) else None
|
||||
return float(di) if isinstance(di, int | float) and not isinstance(di, bool) else None
|
||||
for f in forecasts:
|
||||
di = f.get("deficit_index")
|
||||
if isinstance(di, (int, float)) and not isinstance(di, bool):
|
||||
if isinstance(di, int | float) and not isinstance(di, bool):
|
||||
return float(di)
|
||||
return None
|
||||
|
||||
|
|
@ -314,10 +327,10 @@ def _primary_months_of_inventory(forecasts: Sequence[dict[str, Any]]) -> float |
|
|||
)
|
||||
if primary is not None and primary.get("months_of_inventory") is not None:
|
||||
moi = primary["months_of_inventory"]
|
||||
return float(moi) if isinstance(moi, (int, float)) and not isinstance(moi, bool) else None
|
||||
return float(moi) if isinstance(moi, int | float) and not isinstance(moi, bool) else None
|
||||
for f in forecasts:
|
||||
moi = f.get("months_of_inventory")
|
||||
if isinstance(moi, (int, float)) and not isinstance(moi, bool):
|
||||
if isinstance(moi, int | float) and not isinstance(moi, bool):
|
||||
return float(moi)
|
||||
return None
|
||||
|
||||
|
|
@ -342,7 +355,7 @@ def _overall_score(product_scores: dict[str, Any] | None) -> float | None:
|
|||
if not isinstance(product_scores, dict):
|
||||
return None
|
||||
overall = product_scores.get("overall")
|
||||
if isinstance(overall, (int, float)) and not isinstance(overall, bool):
|
||||
if isinstance(overall, int | float) and not isinstance(overall, bool):
|
||||
return float(overall)
|
||||
return None
|
||||
|
||||
|
|
@ -388,10 +401,10 @@ def _market_now_summary(
|
|||
parts: list[str] = []
|
||||
if market_metrics is not None:
|
||||
velocity = market_metrics.get("unit_velocity")
|
||||
if isinstance(velocity, (int, float)) and not isinstance(velocity, bool):
|
||||
if isinstance(velocity, int | float) and not isinstance(velocity, bool):
|
||||
parts.append(f"абсорбция ~{round(float(velocity), 1)} ед./мес")
|
||||
avg_price = analyze.get("market_avg_price_per_m2")
|
||||
if isinstance(avg_price, (int, float)) and not isinstance(avg_price, bool):
|
||||
if isinstance(avg_price, int | float) and not isinstance(avg_price, bool):
|
||||
parts.append(f"средняя цена ~{round(float(avg_price)):,} ₽/м²".replace(",", " "))
|
||||
# #1634: НЕ через _analog_count — он отдаёт market_metrics.obj_count (число ЖК во
|
||||
# всей district-wide/микрорайонной выборке §9.2), что НЕ равно «конкурентов рядом».
|
||||
|
|
@ -618,6 +631,7 @@ def _build_confidence(
|
|||
market_metrics, future_supply, forecasts, product_scores, special_indices
|
||||
),
|
||||
deal_count=_deal_count(analyze, market_metrics),
|
||||
deal_count_months=_deal_count_months(market_metrics),
|
||||
analog_count=_analog_count(analyze, market_metrics),
|
||||
domrf_coverage=_domrf_coverage(analyze, supply_layers),
|
||||
history_months=_history_months(market_metrics, forecasts),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue