fix(lotto): 학습 게이트 정직화 (engine-best vs random-best 6trial·명시적 gated·정체성 일관)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,7 @@ def test_select_winner_by_lift_gating():
|
||||
{"trial_id": 2, "day_of_week": 1, "weight": [0.3,0.2,0.2,0.2,0.1], "prize_score": 9.0},
|
||||
{"trial_id": 3, "day_of_week": 2, "weight": [0.1,0.3,0.2,0.2,0.2], "prize_score": 4.0},
|
||||
]
|
||||
# random baseline이 8.0이면 lift는 +1, +1, -4 → 노이즈 ε=2 안에서 게이팅
|
||||
# random baseline이 8.0이면 lift는 -3, +1, -4 → 최대 lift(+1) < ε(2) → 게이팅
|
||||
winner = we.select_winner_by_lift(per_w, random_score=8.0, epsilon=2.0)
|
||||
assert winner["gated"] is True # 최대 lift(+1) < ε(2) → 게이팅
|
||||
winner2 = we.select_winner_by_lift(per_w, random_score=3.0, epsilon=2.0)
|
||||
@@ -142,3 +142,57 @@ def test_prize_score_from_hist():
|
||||
s = we.prize_score_from_hist({"m3": 10, "m4": 2, "m5": 0, "m6": 0, "bonus_hits": 0})
|
||||
s_big = we.prize_score_from_hist({"m3": 0, "m4": 0, "m5": 0, "m6": 1, "bonus_hits": 0})
|
||||
assert s_big > s # 1등 1장이 5등 다수보다 큼
|
||||
|
||||
|
||||
def test_select_winner_by_lift_preserves_all_keys():
|
||||
"""select_winner_by_lift는 per_w 항목의 모든 키를 보존해야 한다.
|
||||
best_match, weight_label 등 identity 필드가 누락되면 evaluate_weekly가 깨진다."""
|
||||
per_w = [
|
||||
{
|
||||
"trial_id": 10,
|
||||
"weight_label": "w0",
|
||||
"weight": [0.2] * 5,
|
||||
"prize_score": 3.0,
|
||||
"best_match": 3,
|
||||
},
|
||||
{
|
||||
"trial_id": 11,
|
||||
"weight_label": "w1",
|
||||
"weight": [0.3, 0.2, 0.2, 0.2, 0.1],
|
||||
"prize_score": 20.0,
|
||||
"best_match": 4,
|
||||
},
|
||||
]
|
||||
result = we.select_winner_by_lift(per_w, random_score=5.0, epsilon=2.0)
|
||||
assert result["gated"] is False
|
||||
assert result["trial_id"] == 11
|
||||
assert result["weight_label"] == "w1" # identity 키 보존
|
||||
assert result["best_match"] == 4 # best_match 키 보존
|
||||
assert "lift" in result # lift 추가됨
|
||||
assert result["lift"] == pytest.approx(15.0)
|
||||
|
||||
|
||||
def test_gated_path_keeps_base_via_select_winner():
|
||||
"""gated=True일 때 select_winner_by_lift의 반환값 검증.
|
||||
evaluate_weekly 내의 gated 분기가 올바른 값에 의존함을 확인한다."""
|
||||
per_w = [
|
||||
{"trial_id": 1, "weight_label": "w0", "weight": [0.2]*5,
|
||||
"prize_score": 5.0, "best_match": 2},
|
||||
{"trial_id": 2, "weight_label": "w1", "weight": [0.3,0.2,0.2,0.2,0.1],
|
||||
"prize_score": 7.0, "best_match": 3},
|
||||
]
|
||||
# random_best=8.0 → 최대 engine lift=7-8=-1 → gated
|
||||
result = we.select_winner_by_lift(per_w, random_score=8.0, epsilon=we.LIFT_EPSILON)
|
||||
assert result["gated"] is True
|
||||
assert result["lift"] < 0
|
||||
|
||||
# decide_base_update를 통해 gated가 unchanged를 유도하는지 확인
|
||||
# (gated override가 없더라도, 현재 LIFT_EPSILON=10.0 하에서 lift<0이면 항상 gated)
|
||||
current = [0.2, 0.2, 0.2, 0.2, 0.2]
|
||||
# gated이면 evaluate_weekly가 current_base를 그대로 유지해야 함
|
||||
# 여기서는 override 로직을 직접 재현해 검증한다
|
||||
gated = result["gated"]
|
||||
new_base_override = list(current) if gated else None
|
||||
reason_override = "unchanged_gated" if gated else "should_not_reach"
|
||||
assert new_base_override == current
|
||||
assert reason_override == "unchanged_gated"
|
||||
|
||||
Reference in New Issue
Block a user