검색 결과를 실제 최저가로 정제하는 파이프라인을 핸들러에 결합한다. 검색→필터→이상치→정렬→top-N. AI 유사도 판정 슬롯은 비워둠(키 대기). - pipeline/filters: keep_only_mall·filter_out_malls·filter_by_price_band(요청 현재가 기준 targeted 컷) - pipeline/outliers: IQR 기반 이상치 제거(z-score 대신 — 분포 가정 없음, stdlib만) - pipeline/core: STAGE in/out 관측 로깅 + top-N 최저가(레퍼런스 pipeline_log 계승) - handler: 검색→run_price_pipeline 결합, 요청 price 를 밴드 기준으로 사용 - tests: 정렬/IQR(극단 저·고가 제거)/밴드/mall/빈입력/STAGE/실 fixture 7건 → 전체 27/27 - 라이브 확인: 스탠리 텀블러 40건→top-5 최저가, STAGE 카운트 노출 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 lines
1.3 KiB
Python
33 lines
1.3 KiB
Python
"""가격 이상치 제거.
|
|
|
|
레퍼런스는 반복 z-score(정규분포 가정)를 썼으나, 가격 분포는 한쪽으로 치우친(로그정규) 경우가
|
|
많아 취약하다. 대신 분포 가정이 없는 **IQR(사분위 범위) 방식**을 쓴다(stdlib 만, numpy 불필요).
|
|
목적: '엉뚱한 초저가(액세서리/오매칭)·초고가(묶음)'가 최저가 산정을 오염시키는 것을 막는다.
|
|
"""
|
|
|
|
import statistics
|
|
|
|
from services.search.contract import NormalizedProduct
|
|
|
|
|
|
def remove_price_outliers(
|
|
products: list[NormalizedProduct], k: float = 1.5, min_items: int = 4
|
|
) -> tuple[list[NormalizedProduct], list[NormalizedProduct]]:
|
|
"""IQR 기반 이상치 제거. [Q1 - k·IQR, Q3 + k·IQR] 밖을 제거.
|
|
데이터가 min_items 미만이면 통계가 무의미하므로 그대로 통과. (kept, removed) 반환."""
|
|
prices = [p.price for p in products if p.price and p.price > 0]
|
|
if len(prices) < min_items:
|
|
return list(products), []
|
|
|
|
q1, _, q3 = statistics.quantiles(prices, n=4)
|
|
iqr = q3 - q1
|
|
lo, hi = q1 - k * iqr, q3 + k * iqr
|
|
|
|
kept, removed = [], []
|
|
for p in products:
|
|
if p.price is None or lo <= p.price <= hi:
|
|
kept.append(p)
|
|
else:
|
|
removed.append(p)
|
|
return kept, removed
|