refactoring
This commit is contained in:
273
core/pitch_classifier.py
Normal file
273
core/pitch_classifier.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
core/pitch_classifier.py — 투구/타자 결과 분류
|
||||
|
||||
네이버 리포트 데이터를 기반으로 관리자 사이트에서 선택해야 할
|
||||
라디오 버튼의 라벨(eventName)을 결정합니다.
|
||||
Playwright 의존성 없이 순수 파이썬 로직만 포함합니다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from core.config_loader import (
|
||||
pitch_type_map,
|
||||
pitch_result_map,
|
||||
batter_result_map,
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 구종 분류
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def classify_pitch_type(pitch_type_text: str) -> str | None:
|
||||
"""네이버 구종 텍스트 → 사이트 구종 라벨
|
||||
|
||||
예: '직구' → '패스트볼', '포크' → '포크볼'
|
||||
"""
|
||||
return pitch_type_map().get(pitch_type_text or "")
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 투구 결과 분류
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def classify_pitch_result(pitch_result_text: str) -> str | None:
|
||||
"""네이버 투구결과 텍스트 → 사이트 투구결과 라벨
|
||||
|
||||
예: '볼' → '볼', '스트라이크' → '스트라이크(루킹)'
|
||||
"""
|
||||
return pitch_result_map().get(pitch_result_text or "")
|
||||
|
||||
|
||||
def normalize_pitch_result_code(pitch: dict[str, Any], event: dict[str, Any] | None = None) -> str:
|
||||
"""투구의 pitchResult 코드를 정규화
|
||||
|
||||
피치클락, 번트헛스윙, 폭투/포일 등 특수 상황 처리
|
||||
"""
|
||||
pitch_result = (pitch.get("pitchResult") or "").strip()
|
||||
pitch_result_text = (pitch.get("pitchResultText") or "").strip()
|
||||
normalized_text = pitch_result_text.replace(" ", "")
|
||||
|
||||
# 피치클락 투수위반 → 볼
|
||||
if "피치클락" in pitch_result_text and "투수위반" in pitch_result_text:
|
||||
return "B"
|
||||
|
||||
# 번트 헛스윙 → BS
|
||||
if "번트" in normalized_text and "헛스윙" in normalized_text:
|
||||
return "BS"
|
||||
|
||||
# 폭투/포일 진루 시 → 볼
|
||||
runner_events = _get_pitch_runner_events(pitch, event)
|
||||
if any(
|
||||
re_.get("type") == "wild_pitch_advance" or "폭투" in (re_.get("text") or "")
|
||||
for re_ in runner_events
|
||||
):
|
||||
return "B"
|
||||
if any(
|
||||
re_.get("type") == "passed_ball_advance" or "포일" in (re_.get("text") or "")
|
||||
for re_ in runner_events
|
||||
):
|
||||
return "B"
|
||||
|
||||
return pitch_result
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 타자 결과 분류
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def classify_batter_result(result_type: str) -> str | None:
|
||||
"""결과 타입 코드 → 사이트 타자결과 라벨 (기본 매핑)
|
||||
|
||||
더 복잡한 추론이 필요한 경우 infer_batter_result_label 사용.
|
||||
"""
|
||||
return batter_result_map().get(result_type or "")
|
||||
|
||||
|
||||
def infer_batter_result_label(
|
||||
result: dict[str, Any],
|
||||
event: dict[str, Any] | None = None,
|
||||
) -> str | None:
|
||||
"""타석 결과를 종합적으로 추론하여 사이트 라벨 반환
|
||||
|
||||
result.type, result.text, 주루이벤트, 마지막 투구 등을 모두 분석.
|
||||
"""
|
||||
result_type = result.get("type") or ""
|
||||
result_text = (result.get("text") or "").strip()
|
||||
runner_events = (event or {}).get("runnerEvents") or []
|
||||
last_pitch_result_text = get_last_pitch_result_text(event)
|
||||
|
||||
# 낫아웃
|
||||
if result_type == "strikeout_not_out" or "낫아웃" in result_text:
|
||||
if "폭투" in result_text:
|
||||
return "폭투 낫아웃 진루"
|
||||
if "포일" in result_text:
|
||||
return "포일 낫아웃 진루"
|
||||
if "아웃" in result_text:
|
||||
return "스트라이크-낫아웃"
|
||||
return "낫아웃-출루"
|
||||
|
||||
# 삼진
|
||||
if result_type == "strikeout":
|
||||
if "헛스윙" in last_pitch_result_text or "헛스윙" in result_text:
|
||||
return "스윙 스트라이크-아웃"
|
||||
return "루킹스트라이크-아웃"
|
||||
|
||||
# 희생 번트
|
||||
if "희생 번트" in result_text or "희생번트" in result_text:
|
||||
return "희생 번트"
|
||||
|
||||
# 번트 아웃
|
||||
if "번트 아웃" in result_text or "번트아웃" in result_text:
|
||||
return "번트-아웃"
|
||||
|
||||
# 보크
|
||||
if any(
|
||||
"보크" in (re_.get("text") or "") and "진루" in (re_.get("text") or "")
|
||||
for re_ in runner_events
|
||||
):
|
||||
if "볼" in last_pitch_result_text:
|
||||
return "보크-볼"
|
||||
return "보크"
|
||||
|
||||
# 폭투-볼
|
||||
if any(re_.get("type") == "wild_pitch_advance" for re_ in runner_events):
|
||||
return "폭투-볼"
|
||||
|
||||
# 포볼
|
||||
if result_type == "walk":
|
||||
if any(
|
||||
re_.get("type") == "wild_pitch_advance" or "폭투" in (re_.get("text") or "")
|
||||
for re_ in runner_events
|
||||
):
|
||||
return "폭투-포볼"
|
||||
if any(
|
||||
re_.get("type") == "passed_ball_advance" or "포일" in (re_.get("text") or "")
|
||||
for re_ in runner_events
|
||||
):
|
||||
return "포일-포볼"
|
||||
return "포볼"
|
||||
|
||||
# 포일-볼/스트라이크
|
||||
if any(
|
||||
(re_.get("type") or "") == "passed_ball_advance"
|
||||
for re_ in runner_events
|
||||
):
|
||||
if "볼" in last_pitch_result_text:
|
||||
return "포일-볼"
|
||||
return "포일-스트라이크"
|
||||
|
||||
# 수비실책
|
||||
if result_type == "reach_on_error" or "실책" in result_text:
|
||||
return "수비실책"
|
||||
|
||||
# 야수선택
|
||||
if result_type == "reach_on_fielder_choice":
|
||||
return "야수선택"
|
||||
|
||||
# 땅볼출루
|
||||
if result_type == "reach_on_grounder":
|
||||
return "땅볼출루(무안타)"
|
||||
|
||||
# 병살
|
||||
if result_type == "double_play":
|
||||
if "번트" in result_text:
|
||||
return "번트-병살"
|
||||
return "병살-아웃"
|
||||
|
||||
# N루타 후 주루아웃
|
||||
if result_type == "single_runner_out":
|
||||
return "1루타 후 주루아웃"
|
||||
if result_type == "double_runner_out":
|
||||
return "2루타 후 주루아웃"
|
||||
if result_type == "triple_runner_out":
|
||||
return "3루타 후 주루아웃"
|
||||
|
||||
# N루타 후 수비실책진루
|
||||
if result_type == "single_error_advance":
|
||||
return "1루타 후 수비실책진루"
|
||||
if result_type == "double_error_advance":
|
||||
return "2루타 후 수비실책진루"
|
||||
if result_type == "triple_error_advance":
|
||||
return "3루타 후 수비실책진루"
|
||||
|
||||
# 파울희생플라이
|
||||
if "파울희생플라이" in result_text or "파울 희생플라이" in result_text:
|
||||
return "희생 플라이"
|
||||
|
||||
# 아웃 상세
|
||||
if result_type == "out":
|
||||
if "병살" in result_text:
|
||||
if "번트" in result_text:
|
||||
return "번트-병살"
|
||||
return "병살-아웃"
|
||||
if "희생 플라이" in result_text or "희생플라이" in result_text:
|
||||
return "희생 플라이"
|
||||
if "인필드플라이" in result_text:
|
||||
return "인필드플라이"
|
||||
if "파울플라이" in result_text:
|
||||
return "파울플라이-아웃"
|
||||
return "아웃"
|
||||
|
||||
# 번트안타
|
||||
if result_type == "bunt_hit":
|
||||
return "번트안타"
|
||||
|
||||
# 내야안타
|
||||
if result_type == "single":
|
||||
if "번트안타" in result_text:
|
||||
return "번트안타"
|
||||
if "내야안타" in result_text:
|
||||
return "내야안타"
|
||||
|
||||
# 몸에 맞는 볼
|
||||
if result_type == "hit_by_pitch" or "헤드샷" in result_text:
|
||||
return "몸에 맞는 볼"
|
||||
|
||||
# 기본 매핑 폴백
|
||||
return classify_batter_result(result_type)
|
||||
|
||||
|
||||
def is_simple_terminal_result_type(result_type: str) -> bool:
|
||||
"""팝업 없이 즉시 완료되는 결과 타입인지 확인"""
|
||||
return result_type in {"strikeout", "strikeout_not_out", "walk", "intentional_walk", "hit_by_pitch"}
|
||||
|
||||
|
||||
def is_ball_in_play_event(event: dict[str, Any]) -> bool:
|
||||
"""인플레이 이벤트인지 확인 (마지막 투구가 H)"""
|
||||
pitches = event.get("pitches") or []
|
||||
result = event.get("result") or {}
|
||||
if not pitches or not result:
|
||||
return False
|
||||
return pitches[-1].get("pitchResult") == "H"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 내부 헬퍼
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def _get_pitch_runner_events(
|
||||
pitch: dict[str, Any],
|
||||
event: dict[str, Any] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""투구에 연결된 주루이벤트 반환"""
|
||||
if pitch.get("runnerEvents"):
|
||||
return pitch["runnerEvents"]
|
||||
if event:
|
||||
pitch_num = pitch.get("pitchNum")
|
||||
for re_ in (event.get("runnerEvents") or []):
|
||||
if re_.get("pitchNum") == pitch_num:
|
||||
return [re_]
|
||||
return []
|
||||
|
||||
|
||||
def get_last_pitch_result_text(event: dict[str, Any] | None) -> str:
|
||||
"""이벤트의 마지막 투구 결과 텍스트 반환"""
|
||||
if not event:
|
||||
return ""
|
||||
pitches = event.get("pitches") or []
|
||||
if not pitches:
|
||||
return ""
|
||||
return (pitches[-1].get("pitchResultText") or "").strip()
|
||||
Reference in New Issue
Block a user