test(weight-evolver): 순수 함수 + base update rule 단위 테스트
This commit is contained in:
122
lotto/tests/test_weight_evolver.py
Normal file
122
lotto/tests/test_weight_evolver.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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"
|
||||
Reference in New Issue
Block a user