From d972ea66c3971798e425719350a92475fbd10aa0 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 08:29:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto):=20=EC=B1=84=EC=A0=90=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B0=20=ED=95=A8=EC=88=98=20=E2=80=94=20=EC=9D=BC=EC=B9=98?= =?UTF-8?q?=20=EC=88=98=C2=B7=ED=8C=A8=ED=84=B4=20=EC=9A=94=EC=95=BD=C2=B7?= =?UTF-8?q?=EB=8D=B8=ED=83=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lotto/app/jobs/__init__.py | 0 lotto/app/jobs/grading_helpers.py | 93 +++++++++++++++++++++++++++++ lotto/tests/test_grading_helpers.py | 42 +++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 lotto/app/jobs/__init__.py create mode 100644 lotto/app/jobs/grading_helpers.py create mode 100644 lotto/tests/test_grading_helpers.py diff --git a/lotto/app/jobs/__init__.py b/lotto/app/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lotto/app/jobs/grading_helpers.py b/lotto/app/jobs/grading_helpers.py new file mode 100644 index 0000000..b5d81a3 --- /dev/null +++ b/lotto/app/jobs/grading_helpers.py @@ -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]) diff --git a/lotto/tests/test_grading_helpers.py b/lotto/tests/test_grading_helpers.py new file mode 100644 index 0000000..d794b83 --- /dev/null +++ b/lotto/tests/test_grading_helpers.py @@ -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