Files
web-page-backend/lotto/tests/test_weight_evolver.py
2026-05-31 17:32:37 +09:00

138 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# lotto/tests/test_weight_evolver.py
import json
import math
import pytest
from app import weight_evolver as we
def test_clamp_and_normalize_min_floor():
"""모든 값이 0.05 이상이 되도록 보장 + 합=1.0."""
W = we.clamp_and_normalize([0.01, 0.6, 0.2, 0.1, 0.09])
assert all(w >= 0.05 - 1e-9 for w in W)
assert abs(sum(W) - 1.0) < 1e-9
def test_clamp_and_normalize_negative_becomes_floor():
W = we.clamp_and_normalize([-0.1, 0.5, 0.3, 0.2, 0.1])
assert W[0] >= 0.05 - 1e-9
assert abs(sum(W) - 1.0) < 1e-9
def test_perturbation_changes_around_base():
"""σ=0.05 정규분포 perturbation 후 정규화 — 각 값이 합리적 범위 안."""
base = [0.2, 0.2, 0.2, 0.2, 0.2]
W = we.perturb_weights(base, sigma=0.05, seed=42)
assert abs(sum(W) - 1.0) < 1e-9
assert all(w >= 0.05 - 1e-9 for w in W)
def test_dirichlet_random_distribution():
"""Dirichlet α=2 — 5종 비음수 합=1."""
W = we.dirichlet_weights(alpha=2.0, seed=42)
assert abs(sum(W) - 1.0) < 1e-9
assert all(0.05 - 1e-9 <= w <= 1.0 for w in W)
def test_generate_weekly_candidates_count():
"""6개 후보 생성 — 4 perturb + 2 dirichlet."""
base = [0.2, 0.2, 0.2, 0.2, 0.2]
trials = we.generate_weekly_candidates(base, seed=42)
assert len(trials) == 6
sources = [t["source"] for t in trials]
assert sources.count("perturb") == 4
assert sources.count("dirichlet") == 2
days = sorted(t["day_of_week"] for t in trials)
assert days == [0, 1, 2, 3, 4, 5]
def test_calc_pick_score_six_match():
"""6개 모두 일치 → 1등 → base=1.0 + bonus 1.0 = 2.0."""
score = we.calc_pick_score([1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6])
assert score == pytest.approx(2.0)
def test_calc_pick_score_four_match():
"""4개 일치 → 4등 → base=4/6 + bonus 0.3."""
score = we.calc_pick_score([1, 2, 3, 4, 7, 8], [1, 2, 3, 4, 5, 6])
assert score == pytest.approx(4/6 + 0.3)
def test_calc_pick_score_three_match():
"""3개 일치 → 5등 → base=3/6 + bonus 0.1."""
score = we.calc_pick_score([1, 2, 3, 7, 8, 9], [1, 2, 3, 4, 5, 6])
assert score == pytest.approx(3/6 + 0.1)
def test_calc_pick_score_two_match_no_bonus():
"""2개 일치 → 미당첨 → base=2/6 + bonus 0."""
score = we.calc_pick_score([1, 2, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6])
assert score == pytest.approx(2/6)
def test_decide_base_update_winner_4plus_replaces():
"""winner_max_correct ≥ 4 → 교체."""
current = [0.2, 0.2, 0.2, 0.2, 0.2]
winner_W = [0.1, 0.3, 0.2, 0.3, 0.1]
new_base, reason = we.decide_base_update(
winner_max_correct=4,
winner_W=winner_W,
current_base=current,
)
assert new_base == winner_W
assert reason == "winner_4plus"
def test_decide_base_update_winner_3_ema_blend():
"""winner_max_correct = 3 → 0.3*winner + 0.7*current."""
current = [0.2, 0.2, 0.2, 0.2, 0.2]
winner_W = [0.1, 0.3, 0.2, 0.3, 0.1]
new_base, reason = we.decide_base_update(
winner_max_correct=3,
winner_W=winner_W,
current_base=current,
)
expected = [0.3 * w + 0.7 * c for w, c in zip(winner_W, current)]
assert all(abs(a - b) < 1e-9 for a, b in zip(new_base, expected))
assert reason == "ema_blend"
def test_decide_base_update_winner_lt3_unchanged():
"""winner_max_correct ≤ 2 → 직전 base 유지."""
current = [0.2, 0.2, 0.2, 0.2, 0.2]
winner_W = [0.1, 0.3, 0.2, 0.3, 0.1]
new_base, reason = we.decide_base_update(
winner_max_correct=2,
winner_W=winner_W,
current_base=current,
)
assert new_base == current
assert reason == "unchanged"
def test_decide_base_update_cold_start_returns_default():
"""current_base=None (첫 회) → 균등 default 반환."""
winner_W = [0.1, 0.3, 0.2, 0.3, 0.1]
new_base, reason = we.decide_base_update(
winner_max_correct=4,
winner_W=winner_W,
current_base=None,
)
assert new_base == winner_W
assert reason == "winner_4plus"
def test_select_winner_by_lift_gating():
# engine_w 3개 + random_null 기준. lift = engine 등수점수 random 등수점수
per_w = [
{"trial_id": 1, "day_of_week": 0, "weight": [0.2]*5, "prize_score": 5.0},
{"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 안에서 게이팅
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)
assert winner2["gated"] is False
assert winner2["trial_id"] == 2 # prize 9 → lift +6