fix(estimator): MAD-clip после similarity-weighting + sigma=0 guard (#audit-4)
sigma=0 guard: safe_area_sigma2/safe_floor_sigma2 предотвращает div/0 при нулевом sigma из конфига. Post-weight MAD-clip (estimate_sb_clip_after_weight, дефолт True) выполняется ПОСЛЕ Gaussian weighting — видовые/топ-юниты с высоким ppm² больше не выкидываются до учёта similarity. За флагом.
This commit is contained in:
parent
17edac532b
commit
5b2a39cd5e
1 changed files with 43 additions and 7 deletions
|
|
@ -1721,25 +1721,28 @@ def _compute_same_building_anchor(
|
||||||
if floor_sigma > 0 and floor_target and total_floors_target and total_floors_target > 0:
|
if floor_sigma > 0 and floor_target and total_floors_target and total_floors_target > 0:
|
||||||
target_pos = floor_target / total_floors_target
|
target_pos = floor_target / total_floors_target
|
||||||
|
|
||||||
|
# #audit-4: sigma > 0 guard — при sigma=0 Gaussian exp(-x²/0) → div/0/NaN.
|
||||||
|
# area_sigma=0 (отключено) → нейтральный вес 1.0; floor_sigma=0 уже гейтится выше.
|
||||||
|
safe_area_sigma2 = 2.0 * sigma * sigma if sigma > 0 else 0.0
|
||||||
|
safe_floor_sigma2 = 2.0 * floor_sigma * floor_sigma if floor_sigma > 0 else 0.0
|
||||||
|
|
||||||
# 1. similarity weights (area × rooms × floor-position)
|
# 1. similarity weights (area × rooms × floor-position)
|
||||||
weights: list[float] = []
|
weights: list[float] = []
|
||||||
for c in comps:
|
for c in comps:
|
||||||
a = c.get("area_m2")
|
a = c.get("area_m2")
|
||||||
if a and area_target > 0:
|
if a and area_target > 0 and safe_area_sigma2 > 0:
|
||||||
area_w = math.exp(-((math.log(a / area_target)) ** 2) / (2.0 * sigma * sigma))
|
area_w = math.exp(-((math.log(a / area_target)) ** 2) / safe_area_sigma2)
|
||||||
else:
|
else:
|
||||||
area_w = 1.0 # площадь неизвестна → нейтральный area-вес
|
area_w = 1.0 # площадь неизвестна или sigma=0 → нейтральный area-вес
|
||||||
rooms_match = rooms_target is not None and c.get("rooms") == rooms_target
|
rooms_match = rooms_target is not None and c.get("rooms") == rooms_target
|
||||||
rooms_w = rooms_boost if rooms_match else 1.0
|
rooms_w = rooms_boost if rooms_match else 1.0
|
||||||
floor_w = 1.0
|
floor_w = 1.0
|
||||||
if target_pos is not None:
|
if target_pos is not None and safe_floor_sigma2 > 0:
|
||||||
cf = c.get("floor")
|
cf = c.get("floor")
|
||||||
ctf = c.get("total_floors")
|
ctf = c.get("total_floors")
|
||||||
if cf and ctf and ctf > 0:
|
if cf and ctf and ctf > 0:
|
||||||
comp_pos = cf / ctf
|
comp_pos = cf / ctf
|
||||||
floor_w = math.exp(
|
floor_w = math.exp(-((comp_pos - target_pos) ** 2) / safe_floor_sigma2)
|
||||||
-((comp_pos - target_pos) ** 2) / (2.0 * floor_sigma * floor_sigma)
|
|
||||||
)
|
|
||||||
# компл без floor/total_floors → нейтральный floor-вес 1.0
|
# компл без floor/total_floors → нейтральный floor-вес 1.0
|
||||||
weights.append(area_w * rooms_w * floor_w)
|
weights.append(area_w * rooms_w * floor_w)
|
||||||
wsum = sum(weights)
|
wsum = sum(weights)
|
||||||
|
|
@ -1748,6 +1751,39 @@ def _compute_same_building_anchor(
|
||||||
else:
|
else:
|
||||||
anchor = _percentile(sorted(ppm2), 0.5)
|
anchor = _percentile(sorted(ppm2), 0.5)
|
||||||
|
|
||||||
|
# #audit-4: MAD-clip ПОСЛЕ similarity-weighting (за флагом estimate_sb_clip_after_weight).
|
||||||
|
# Видовые/топ-юниты с высоким ppm² могут быть выкинуты сырым clip'ом ДО weighting —
|
||||||
|
# они легитимны. После weighting (anchor = weighted mean) ищем выбросы
|
||||||
|
# относительно self-consistent взвешенного пространства.
|
||||||
|
# Используем те же effective_mad_k; если после clip < min_comps → anchor=None.
|
||||||
|
if settings.estimate_sb_clip_after_weight and n >= 2:
|
||||||
|
post_clip_idx = _mad_clip(ppm2, effective_mad_k)
|
||||||
|
if len(post_clip_idx) < min_comps:
|
||||||
|
logger.info(
|
||||||
|
"anchor post-weight MAD-clip #audit-4: %d comps → %d survived"
|
||||||
|
" (< min_comps=%d) → fallback",
|
||||||
|
n,
|
||||||
|
len(post_clip_idx),
|
||||||
|
min_comps,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if len(post_clip_idx) < n:
|
||||||
|
logger.info(
|
||||||
|
"anchor post-weight MAD-clip #audit-4: %d comps → %d after k=%.1f×MAD",
|
||||||
|
n,
|
||||||
|
len(post_clip_idx),
|
||||||
|
effective_mad_k,
|
||||||
|
)
|
||||||
|
comps = [comps[i] for i in post_clip_idx]
|
||||||
|
ppm2 = [ppm2[i] for i in post_clip_idx]
|
||||||
|
weights = [weights[i] for i in post_clip_idx]
|
||||||
|
n = len(ppm2)
|
||||||
|
wsum = sum(weights)
|
||||||
|
if wsum > 0:
|
||||||
|
anchor = sum(w * p for w, p in zip(weights, ppm2, strict=True)) / wsum
|
||||||
|
else:
|
||||||
|
anchor = _percentile(sorted(ppm2), 0.5)
|
||||||
|
|
||||||
# 2. premium uplift — топ-юнит дома (площадь ≥ p66 И комнаты ≥ медианы) И Tier A
|
# 2. premium uplift — топ-юнит дома (площадь ≥ p66 И комнаты ≥ медианы) И Tier A
|
||||||
# → weighted p70. Условие по комнатам отсекает мелкие юниты во флагман-домах.
|
# → weighted p70. Условие по комнатам отсекает мелкие юниты во флагман-домах.
|
||||||
used_uplift = False
|
used_uplift = False
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue