feat(lotto): 채점 보조 함수 — 일치 수·패턴 요약·델타
This commit is contained in:
0
lotto/app/jobs/__init__.py
Normal file
0
lotto/app/jobs/__init__.py
Normal file
93
lotto/app/jobs/grading_helpers.py
Normal file
93
lotto/app/jobs/grading_helpers.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""채점 보조 — 일치 수 계산, 패턴 요약, 패턴 갭."""
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
LOW_HIGH_CUT = 22 # curator_helpers.py 와 동일
|
||||||
|
|
||||||
|
|
||||||
|
def score_picks_against_draw(picks: List[Dict[str, Any]],
|
||||||
|
win_nums: List[int],
|
||||||
|
bonus: int) -> Dict[str, Any]:
|
||||||
|
"""4계층 중 한 그룹(예: core_picks 5세트) vs 추첨 결과 채점.
|
||||||
|
|
||||||
|
picks 는 [{numbers, risk_tag, reason}] 리스트.
|
||||||
|
"""
|
||||||
|
if not picks:
|
||||||
|
return {"avg_match": None, "best_match": 0, "five_plus_prizes": 0, "best_tier": None}
|
||||||
|
|
||||||
|
win_set = set(win_nums)
|
||||||
|
matches = []
|
||||||
|
for p in picks:
|
||||||
|
nums = p.get("numbers") or []
|
||||||
|
m = len(set(nums) & win_set)
|
||||||
|
matches.append((m, p.get("risk_tag")))
|
||||||
|
|
||||||
|
avg = sum(m for m, _ in matches) / len(matches)
|
||||||
|
best_match, best_tier = max(matches, key=lambda x: x[0])
|
||||||
|
five_plus = sum(1 for m, _ in matches if m >= 3) # 5등 이상
|
||||||
|
|
||||||
|
# tier별 평균 → 가장 잘 맞은 risk_tag
|
||||||
|
tier_scores: Dict[str, List[int]] = {}
|
||||||
|
for m, t in matches:
|
||||||
|
if t:
|
||||||
|
tier_scores.setdefault(t, []).append(m)
|
||||||
|
if tier_scores:
|
||||||
|
best_tier = max(tier_scores.items(),
|
||||||
|
key=lambda kv: sum(kv[1]) / len(kv[1]))[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_match": round(avg, 2),
|
||||||
|
"best_match": best_match,
|
||||||
|
"five_plus_prizes": five_plus,
|
||||||
|
"best_tier": best_tier,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_pattern(nums: List[int]) -> Dict[str, int]:
|
||||||
|
"""한 세트의 패턴 요약 — 저/고, 홀/짝, 합계."""
|
||||||
|
nums = sorted(nums)
|
||||||
|
odd = sum(1 for n in nums if n % 2 == 1)
|
||||||
|
low = sum(1 for n in nums if n <= LOW_HIGH_CUT)
|
||||||
|
return {
|
||||||
|
"odd_count": odd,
|
||||||
|
"even_count": 6 - odd,
|
||||||
|
"low_count": low,
|
||||||
|
"high_count": 6 - low,
|
||||||
|
"sum": sum(nums),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_pattern_summaries(summaries: List[Dict[str, int]]) -> Dict[str, float]:
|
||||||
|
"""여러 세트의 패턴 요약 → 평균(low_avg, odd_avg, sum_avg)."""
|
||||||
|
if not summaries:
|
||||||
|
return {"low_avg": None, "odd_avg": None, "sum_avg": None}
|
||||||
|
n = len(summaries)
|
||||||
|
return {
|
||||||
|
"low_avg": round(sum(s["low_count"] for s in summaries) / n, 2),
|
||||||
|
"odd_avg": round(sum(s["odd_count"] for s in summaries) / n, 2),
|
||||||
|
"sum_avg": round(sum(s["sum"] for s in summaries) / n, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_pattern_delta(user_summary: Dict[str, float],
|
||||||
|
draw_summary: Dict[str, float]) -> str:
|
||||||
|
"""사용자 평균 vs 추첨 패턴의 가장 큰 격차 1~2개를 한 줄로."""
|
||||||
|
if not user_summary or user_summary.get("low_avg") is None:
|
||||||
|
return ""
|
||||||
|
deltas = []
|
||||||
|
if user_summary.get("low_avg") is not None and draw_summary.get("low_avg") is not None:
|
||||||
|
d = round(user_summary["low_avg"] - draw_summary["low_avg"], 2)
|
||||||
|
if abs(d) >= 0.5:
|
||||||
|
sign = "+" if d > 0 else ""
|
||||||
|
deltas.append(("저번호", d, f"저번호 편향 {sign}{d}"))
|
||||||
|
if user_summary.get("sum_avg") is not None and draw_summary.get("sum_avg") is not None:
|
||||||
|
d = round(user_summary["sum_avg"] - draw_summary["sum_avg"], 1)
|
||||||
|
if abs(d) >= 10:
|
||||||
|
sign = "+" if d > 0 else ""
|
||||||
|
deltas.append(("합계", d, f"합계 {sign}{d}"))
|
||||||
|
if user_summary.get("odd_avg") is not None and draw_summary.get("odd_avg") is not None:
|
||||||
|
d = round(user_summary["odd_avg"] - draw_summary["odd_avg"], 2)
|
||||||
|
if abs(d) >= 0.5:
|
||||||
|
sign = "+" if d > 0 else ""
|
||||||
|
deltas.append(("홀짝", d, f"홀짝 {sign}{d}"))
|
||||||
|
deltas.sort(key=lambda x: -abs(x[1]))
|
||||||
|
return " / ".join(d[2] for d in deltas[:2])
|
||||||
42
lotto/tests/test_grading_helpers.py
Normal file
42
lotto/tests/test_grading_helpers.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
from app.jobs.grading_helpers import (
|
||||||
|
score_picks_against_draw,
|
||||||
|
summarize_pattern,
|
||||||
|
compute_pattern_delta,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_picks_against_draw_basic():
|
||||||
|
win_nums = [3, 11, 17, 25, 33, 41]
|
||||||
|
bonus = 8
|
||||||
|
picks = [
|
||||||
|
{"numbers": [3, 11, 17, 25, 33, 41], "risk_tag": "안정"}, # 6 일치
|
||||||
|
{"numbers": [1, 2, 3, 4, 5, 6], "risk_tag": "공격"}, # 1 일치
|
||||||
|
{"numbers": [3, 11, 17, 4, 5, 6], "risk_tag": "안정"}, # 3 일치 → 5등
|
||||||
|
]
|
||||||
|
out = score_picks_against_draw(picks, win_nums, bonus)
|
||||||
|
# 함수가 round(avg, 2) 로 반환하므로 rounded 비교
|
||||||
|
assert out["avg_match"] == 3.33
|
||||||
|
assert out["best_match"] == 6
|
||||||
|
assert out["five_plus_prizes"] == 2 # 3개 이상 카운트(5등 이상)
|
||||||
|
assert out["best_tier"] == "안정"
|
||||||
|
|
||||||
|
|
||||||
|
def test_summarize_pattern():
|
||||||
|
nums = [3, 11, 17, 25, 33, 41]
|
||||||
|
s = summarize_pattern(nums)
|
||||||
|
# 저번호(<=22) 3개, 고번호 3개, 모두 홀수이므로 홀:짝 = 6:0
|
||||||
|
assert s["low_count"] == 3
|
||||||
|
assert s["odd_count"] == 6
|
||||||
|
assert s["sum"] == 130
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_pattern_delta_picks_dominant_axis():
|
||||||
|
# 사용자가 평균 저번호 4.2개 / 추첨 평균 3 → 저번호 편향 +1.2
|
||||||
|
user = {"low_avg": 4.2, "odd_avg": 3.4, "sum_avg": 124}
|
||||||
|
draw = {"low_avg": 3.0, "odd_avg": 3.0, "sum_avg": 142}
|
||||||
|
delta = compute_pattern_delta(user, draw)
|
||||||
|
assert "저번호" in delta or "low" in delta
|
||||||
|
assert "+1.2" in delta or "1.2" in delta
|
||||||
Reference in New Issue
Block a user