"""probe_serving_dqn — 서빙 번들(dqn_serving.npz)의 상황별 카드 선택 프로브. 배포된 모델이 '상황에 맞게' 고르는지 눈으로 확인하는 진단 도구: 협력사 세그먼트 × 고객사 성향 × 협상 국면(가격대)별 선택 카드를 표로 출력한다. 전부 다르길 기대하는 게 아니라, 축을 바꿨을 때 선택이 '움직이는지'를 본다. 실행: APP_ENV=local python -m tools.probe_serving_dqn (numpy 만 필요, DB 불필요) """ import numpy as np from negotiation.qtable.domain.model.snapshot import NegotiationSnapshot from negotiation.qtable.domain.service.feature_builder import build_state_features, build_tenant_features from tenancy.config_loader import TenantConfigLoader from tools.export_dqn_serving import OUT_PATH from tools.retrain_from_logs import np_scorer_from_bundle from tools.train_feature_dqn import pref_config ANCHOR, TARGET = 495_000.0, 500_000.0 # BUGCHECK 견적과 동일 스케일 SUPPLIERS = { "소형·경쟁多": dict(revenue_amount=5_000_000, partner_count=3, distribution_code="A"), "소형·단독": dict(revenue_amount=5_000_000, partner_count=1, distribution_code="A"), "대형·경쟁多": dict(revenue_amount=200_000_000, partner_count=3, distribution_code="A"), "대형·단독": dict(revenue_amount=200_000_000, partner_count=1, distribution_code="A"), } PHASES = { # (라운드, 제시가): 첫턴 높은 가격 / 중반 목표가 근접 / 막판 앵커존 직전 "첫턴(575k)": (1, 575_000.0), "중반(510k)": (2, 510_000.0), "막판(501k)": (3, 501_000.0), } PREFS = {"성사중시": 0.1, "가격중시": 0.9} def main(): score = np_scorer_from_bundle(OUT_PATH) z = np.load(OUT_PATH, allow_pickle=False) numbers = [str(n) for n in z["card_numbers"]] feats = z["card_feats"] base = TenantConfigLoader().load("ktcommerce").reward for phase, (turn, price) in PHASES.items(): print(f"\n=== {phase} (앵커 {int(ANCHOR):,} / 목표 {int(TARGET):,}) ===") print(f"{'협력사':<12}" + "".join(f"{p:>16}" for p in PREFS)) for sup_name, sup in SUPPLIERS.items(): row = [] for _, p in PREFS.items(): tf = build_tenant_features(pref_config(base, p)) snap = NegotiationSnapshot( revenue_amount=sup["revenue_amount"], distribution_code=sup["distribution_code"], partner_count=sup["partner_count"], acceptance_ratio=max(0.0, (575_000.0 - price) / 575_000.0), input_price=price, anchor_price=ANCHOR, target_price=TARGET, round_number=turn, ) sf = np.concatenate([build_state_features(snap), tf]) row.append(numbers[int(np.argmax(score(sf, feats)))]) print(f"{sup_name:<12}" + "".join(f"{c:>16}" for c in row)) if __name__ == "__main__": main()