fix(estimator): data-age guards — sber staleness + IMV thin-market (#audit-5)
5a: _load_sber_index_series логирует warning если latest месяц серии старее sber_index_max_age_days (дефолт 35) — расчёт не блокируется. 5b: AvitoImvSummary.thin_market (bool, дефолт False) — True когда market_count < avito_imv_thin_market_threshold (дефолт 10). Все три точки конструирования AvitoImvSummary обновлены. Warning при thin.
This commit is contained in:
parent
5b2a39cd5e
commit
a6ef2c73e1
2 changed files with 50 additions and 7 deletions
|
|
@ -98,6 +98,10 @@ class AvitoImvSummary(BaseModel):
|
|||
lower_price: int | None = None # нижняя граница IMV-коридора, ₽
|
||||
higher_price: int | None = None # верхняя граница IMV-коридора, ₽
|
||||
market_count: int | None = None # объём рынка, на котором построена оценка
|
||||
# #audit-5b: тонкий рынок — market_count < avito_imv_thin_market_threshold.
|
||||
# True = IMV построен на малой выборке → reliability ↓. Фронт/estimator могут
|
||||
# использовать для понижения уверенности или отображения предупреждения.
|
||||
thin_market: bool = False
|
||||
|
||||
|
||||
class DkpCorridor(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1102,6 +1102,7 @@ def _load_sber_index_series(db: Session, *, region: str) -> dict[date, float]:
|
|||
"""#794: monthly {period_month: index_value} for region from sber_price_index.
|
||||
|
||||
Tries SBER_COEFF_DASHBOARDS in order; returns first non-empty series. {} on any error.
|
||||
#audit-5a: если latest месяц серии старее sber_index_max_age_days → warning.
|
||||
"""
|
||||
for dash in SBER_COEFF_DASHBOARDS:
|
||||
try:
|
||||
|
|
@ -1123,7 +1124,23 @@ def _load_sber_index_series(db: Session, *, region: str) -> dict[date, float]:
|
|||
logger.warning("sber_price_index lookup failed for %s (graceful): %s", dash, exc)
|
||||
continue
|
||||
if rows:
|
||||
return {r["period_month"]: float(r["index_value_rub_m2"]) for r in rows}
|
||||
series = {r["period_month"]: float(r["index_value_rub_m2"]) for r in rows}
|
||||
# #audit-5a: data-age guard — предупреждаем о stale СберИндексе.
|
||||
latest = max(series)
|
||||
today = datetime.now(tz=UTC).date()
|
||||
age_days = (today - latest).days
|
||||
if age_days > settings.sber_index_max_age_days:
|
||||
logger.warning(
|
||||
"sber_index stale #audit-5a: latest=%s age=%d days"
|
||||
" (> sber_index_max_age_days=%d) region=%s dash=%s"
|
||||
" — time-adjustment may be outdated",
|
||||
latest.isoformat(),
|
||||
age_days,
|
||||
settings.sber_index_max_age_days,
|
||||
region,
|
||||
dash,
|
||||
)
|
||||
return series
|
||||
return {}
|
||||
|
||||
|
||||
|
|
@ -2492,14 +2509,17 @@ async def estimate_quality(
|
|||
int(imv_anchor["higher_price"]) if imv_anchor.get("higher_price") else None
|
||||
)
|
||||
anchor_label = "оценке Avito IMV"
|
||||
_imv_mc = int(imv_anchor["market_count"]) if imv_anchor.get("market_count") else None
|
||||
avito_imv_summary = AvitoImvSummary(
|
||||
recommended_price=anchor_total,
|
||||
lower_price=(
|
||||
int(imv_anchor["lower_price"]) if imv_anchor.get("lower_price") else None
|
||||
),
|
||||
higher_price=anchor_higher,
|
||||
market_count=(
|
||||
int(imv_anchor["market_count"]) if imv_anchor.get("market_count") else None
|
||||
market_count=_imv_mc,
|
||||
# #audit-5b: тонкий рынок — малая выборка для IMV.
|
||||
thin_market=(
|
||||
_imv_mc is not None and _imv_mc < settings.avito_imv_thin_market_threshold
|
||||
),
|
||||
)
|
||||
elif imv_eval is not None and imv_eval.recommended_price:
|
||||
|
|
@ -2512,6 +2532,20 @@ async def estimate_quality(
|
|||
lower_price=int(imv_eval.lower_price) if imv_eval.lower_price else None,
|
||||
higher_price=anchor_higher,
|
||||
market_count=imv_eval.market_count,
|
||||
# #audit-5b: тонкий рынок — market_count < threshold.
|
||||
thin_market=(
|
||||
imv_eval.market_count is not None
|
||||
and imv_eval.market_count < settings.avito_imv_thin_market_threshold
|
||||
),
|
||||
)
|
||||
|
||||
# #audit-5b: thin-market warning — IMV на малой выборке → reliability ↓.
|
||||
if avito_imv_summary is not None and avito_imv_summary.thin_market:
|
||||
logger.warning(
|
||||
"avito_imv thin_market #audit-5b: market_count=%s"
|
||||
" (< avito_imv_thin_market_threshold=%d) — IMV reliability low",
|
||||
avito_imv_summary.market_count,
|
||||
settings.avito_imv_thin_market_threshold,
|
||||
)
|
||||
|
||||
if anchor_total is not None:
|
||||
|
|
@ -2556,6 +2590,11 @@ async def estimate_quality(
|
|||
area=payload.area_m2,
|
||||
)
|
||||
if imv_anchor_disp is not None and imv_anchor_disp.get("recommended_price"):
|
||||
_disp_mc = (
|
||||
int(imv_anchor_disp["market_count"])
|
||||
if imv_anchor_disp.get("market_count")
|
||||
else None
|
||||
)
|
||||
avito_imv_summary = AvitoImvSummary(
|
||||
recommended_price=int(imv_anchor_disp["recommended_price"]),
|
||||
lower_price=(
|
||||
|
|
@ -2568,10 +2607,10 @@ async def estimate_quality(
|
|||
if imv_anchor_disp.get("higher_price")
|
||||
else None
|
||||
),
|
||||
market_count=(
|
||||
int(imv_anchor_disp["market_count"])
|
||||
if imv_anchor_disp.get("market_count")
|
||||
else None
|
||||
market_count=_disp_mc,
|
||||
# #audit-5b: тонкий рынок.
|
||||
thin_market=(
|
||||
_disp_mc is not None and _disp_mc < settings.avito_imv_thin_market_threshold
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue