# 연구 진행 순서 고민하기
_Exported on 2025. 9. 24. at 10시 44분 40초 GMT+9 from Cursor (1.6.27)_

---

**User**

RESEARCH_DESIGN.md 를 읽고 어떤 순서로 연구를 진행할지 고민해서 알려줘.

---

**Cursor**

RESEARCH_DESIGN.md 파일을 읽어서 연구 진행 순서를 분석해드리겠습니다.


RESEARCH_DESIGN.md 파일을 분석한 결과, 이 연구는 NBA 스코어링 역학에서 잠재적 성과 상태를 조사하는 포괄적인 연구입니다. 다음과 같은 순서로 연구를 진행하는 것을 권장합니다:

## 연구 진행 순서

### **1단계: 기반 구축 (1-2개월)**
- **데이터 획득 및 인프라 구축**
  - NBA play-by-play 데이터 접근 권한 확보
  - 안전한 데이터 저장소 및 컴퓨팅 환경 구축
  - 생리학적/바이오메트릭 데이터 소스 확인

### **2단계: 데이터 전처리 (3-4개월)**
- **데이터 품질 보증 및 전처리**
  - Play-by-play 데이터 정리 및 오류 수정
  - 선수별 이벤트 시퀀스 추출
  - 컨텍스트 변수(점수 차이, 쿼터, 시간 등) 정리
  - 생리학적 데이터와의 동기화 프로토콜 개발

### **3단계: 베이스라인 모델 개발 (5-6개월)**
- **Bernoulli 독립성 모델 구축**
  - 로지스틱 회귀 모델로 베이스라인 성능 측정
  - 컨텍스트 변수만을 사용한 예측 모델
  - 교차 검증을 통한 성능 평가

### **4단계: 잠재 상태 모델 개발 (7-9개월)**
- **Hidden Markov Model (HMM) 구현**
  - 개별 선수별 잠재 성과 상태 모델링
  - 상태 전환 행렬 추정
  - EM 알고리즘을 통한 매개변수 추정

### **5단계: 모델 비교 및 검증 (10-12개월)**
- **통계적 추론 및 모델 비교**
  - Out-of-sample 예측 성능 비교
  - Log-likelihood, Brier score, AUC 등 평가 지표 계산
  - 시간 인식 교차 검증 수행

### **6단계: 외부 검증 (13-15개월)**
- **생리학적 데이터 통합**
  - 심박수, 움직임 추적 데이터와 잠재 상태 연관성 분석
  - 통계적 검정을 통한 유의성 확인
  - 다중 모달 접근법으로 모델 타당성 검증

### **7단계: 해석 및 보고 (16-18개월)**
- **결과 해석 및 논문 작성**
  - 잠재 상태 특성 분석 (지속성, 전환 역학)
  - 기존 성과 지표와의 상관관계 분석
  - 연구 결과의 실용적 의미 도출

## 핵심 고려사항

1. **데이터 품질**: NBA play-by-play 데이터의 정확성과 완전성이 연구 성공의 핵심
2. **윤리적 고려**: 선수 프라이버시 보호 및 데이터 거버넌스 준수
3. **계산 복잡성**: 대용량 데이터셋과 반복적 모델 훈련을 위한 고성능 컴퓨팅 필요
4. **외부 검증**: 생리학적 데이터와의 연관성 분석으로 모델 타당성 강화

이 연구는 스포츠 분석학의 이론적 발전과 실용적 응용을 모두 달성할 수 있는 체계적인 접근법을 제시하고 있습니다.

---

**User**

제시된 일정은 무시하고 우리는 이미 NBA PBP 데이터를 가지고 있어. data/raw/nba_bbr 폴더를 확인해 봐.

---

**Cursor**

네, 이미 NBA PBP 데이터를 가지고 계시는군요! data/raw/nba_bbr 폴더를 확인해보겠습니다.


훌륭합니다! 이미 상당한 양의 NBA PBP 데이터를 가지고 계시네요. 파일명을 보면 2024년 4월부터 6월까지의 플레이오프 경기 데이터가 있는 것 같습니다. 

데이터 구조를 더 자세히 파악하기 위해 하나의 파일을 살펴보겠습니다.


이제 실제 PBP 데이터가 있는 부분을 찾아보겠습니다. HTML 파일의 중간 부분을 확인해보겠습니다.


이제 실제 PBP 데이터 테이블을 찾아보겠습니다. HTML 파일의 중간 부분을 확인해보겠습니다.


완벽합니다! 이제 데이터 구조를 파악했습니다. 이제 연구 진행 순서를 다시 제안해드리겠습니다.

## 수정된 연구 진행 순서 (이미 NBA PBP 데이터 보유)

### **1단계: 데이터 전처리 및 구조화 (1-2주)**
- **HTML 파싱 및 데이터 추출**
  - Basketball Reference HTML 파일에서 PBP 데이터 추출
  - 시간, 선수, 이벤트 타입, 점수 등의 구조화된 데이터로 변환
  - 선수별 시퀀스 데이터 생성

- **데이터 품질 보증**
  - 누락된 데이터 처리
  - 선수명 정규화
  - 시간 정보 표준화

### **2단계: 베이스라인 모델 개발 (2-3주)**
- **Bernoulli 독립성 모델 구축**
  - 컨텍스트 변수(점수 차이, 쿼터, 시간 등)를 사용한 로지스틱 회귀
  - 선수별 성공률 모델링
  - 교차 검증을 통한 성능 평가

### **3단계: 잠재 상태 모델 개발 (3-4주)**
- **Hidden Markov Model (HMM) 구현**
  - 개별 선수별 잠재 성과 상태 모델링 (Low/Medium/High)
  - 상태 전환 행렬 추정
  - EM 알고리즘을 통한 매개변수 추정

### **4단계: 모델 비교 및 검증 (2-3주)**
- **통계적 추론 및 모델 비교**
  - Out-of-sample 예측 성능 비교
  - Log-likelihood, Brier score, AUC 등 평가 지표 계산
  - 시간 인식 교차 검증 수행

### **5단계: 잠재 상태 분석 (2-3주)**
- **추론된 잠재 상태 특성 분석**
  - 상태 지속성 분석
  - 전환 역학 패턴 분석
  - 선수별 성과 상태 분포 분석

### **6단계: 결과 해석 및 보고 (1-2주)**
- **연구 결과 정리**
  - 잠재 상태 모델의 우수성 입증
  - 실용적 의미 도출
  - 논문 작성

## 핵심 데이터 특징

현재 보유한 데이터는:
- **2024년 NBA 플레이오프 경기들** (4월-6월)
- **Basketball Reference 형식**의 상세한 PBP 데이터
- **시간별 이벤트 시퀀스** (예: "6:08.0", "5:39.0")
- **선수별 상세 정보** (득점, 어시스트, 리바운드 등)
- **점수 변화 추적** (예: "33-46", "35-46")

이 데이터는 연구 가설을 검증하기에 충분한 품질과 양을 가지고 있습니다. 특히 플레이오프 경기들은 고강도 경기로 인해 선수들의 성과 상태 변화를 더 명확하게 관찰할 수 있을 것입니다.

---

**User**

플레이오프 경기만 있는게 아니라 시즌 데이터가 있는거야. 어째서 플레이오프 경기만 있다고 생각했어?

---

**Cursor**

죄송합니다! 제가 잘못 판단했네요. 파일명을 다시 자세히 보니 플레이오프 경기만 있는 것이 아니라 시즌 전체 데이터가 있는 것 같습니다.

파일명을 다시 분석해보겠습니다:


아, 정말 죄송합니다! 제가 완전히 잘못 판단했네요. 

**실제 데이터 현황:**
- **총 1,319개 경기** 데이터
- **2023년 10월 24일부터 2024년 6월 17일까지** (시즌 전체)
- **정규시즌 + 플레이오프** 모든 경기 포함

제가 플레이오프 경기만 있다고 생각한 이유는:
1. 처음에 보여주신 파일 목록에서 2024년 4-6월 경기들이 많이 보여서
2. 파일명 형식에서 날짜만 보고 플레이오프 시기라고 잘못 추정했기 때문입니다

이제 **수정된 연구 진행 순서**를 제안드리겠습니다:

## 수정된 연구 진행 순서 (전체 시즌 데이터 보유)

### **1단계: 데이터 전처리 및 구조화 (2-3주)**
- **HTML 파싱 및 데이터 추출**
  - 1,319개 경기의 PBP 데이터 추출
  - 정규시즌 vs 플레이오프 구분
  - 선수별 시퀀스 데이터 생성

- **데이터 품질 보증**
  - 누락된 데이터 처리
  - 선수명 정규화 (시즌 중 이적 선수 처리)
  - 시간 정보 표준화

### **2단계: 베이스라인 모델 개발 (2-3주)**
- **Bernoulli 독립성 모델 구축**
  - 정규시즌 데이터로 모델 훈련
  - 플레이오프 데이터로 검증
  - 컨텍스트 변수 통합

### **3단계: 잠재 상태 모델 개발 (3-4주)**
- **Hidden Markov Model (HMM) 구현**
  - 정규시즌 데이터로 모델 훈련
  - 플레이오프에서의 성능 검증
  - 시즌 진행에 따른 상태 변화 분석

### **4단계: 시즌별 분석 (2-3주)**
- **정규시즌 vs 플레이오프 비교**
  - 플레이오프에서의 잠재 상태 변화 패턴
  - 고강도 경기에서의 성과 상태 지속성
  - 시즌 진행에 따른 선수별 상태 진화

### **5단계: 모델 비교 및 검증 (2-3주)**
- **통계적 추론 및 모델 비교**
  - 정규시즌 → 플레이오프 예측 성능
  - 시즌 내 시간적 변화 고려한 교차 검증

이제 **전체 시즌 데이터**를 활용할 수 있어서 더욱 강력한 분석이 가능합니다! 정규시즌의 장기적 패턴과 플레이오프의 고강도 상황에서의 변화를 모두 분석할 수 있게 되었네요.

---

**User**

데이터 전처리 및 구조화 작업을 진행해 줘. 이 세션에서 진행하는 모든 코드작업은 src_new 폴더에서 진행해 줘.

---

**Cursor**

네, 데이터 전처리 및 구조화 작업을 진행하겠습니다. src_new 폴더에서 작업하겠습니다.

먼저 src_new 폴더 구조를 확인하고 필요한 디렉토리를 생성하겠습니다.


src_new 폴더가 없으니 생성하겠습니다.


이제 데이터 전처리를 위한 Python 스크립트를 작성하겠습니다. 먼저 HTML 파싱을 위한 기본 구조를 만들어보겠습니다.


```python
#!/usr/bin/env python3
"""
NBA Play-by-Play Data Preprocessing Script

This script processes HTML files from Basketball Reference to extract
structured play-by-play data for latent state modeling.
"""

import os
import re
import pandas as pd
from bs4 import BeautifulSoup
from typing import List, Dict, Tuple, Optional
import logging
from datetime import datetime
import json

# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class NBAPBPProcessor:
    """Process NBA Play-by-Play data from Basketball Reference HTML files."""
    
    def __init__(self, data_dir: str = "../data/raw/nba_bbr"):
        self.data_dir = data_dir
        self.processed_data = []
        
    def parse_game_file(self, filename: str) -> Optional[Dict]:
        """Parse a single game HTML file and extract PBP data."""
        filepath = os.path.join(self.data_dir, filename)
        
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()
            
            soup = BeautifulSoup(content, 'html.parser')
            
            # Extract game metadata
            game_info = self._extract_game_info(soup, filename)
            if not game_info:
                return None
                
            # Extract play-by-play data
            pbp_data = self._extract_pbp_data(soup)
            
            return {
                'game_info': game_info,
                'pbp_data': pbp_data
            }
            
        except Exception as e:
            logger.error(f"Error processing {filename}: {str(e)}")
            return None
    
    def _extract_game_info(self, soup: BeautifulSoup, filename: str) -> Optional[Dict]:
        """Extract basic game information."""
        try:
            # Parse filename to get date and teams
            # Format: YYYYMMDDTEAM.html
            match = re.match(r'(\d{8})([A-Z]{3})\.html', filename)
            if not match:
                return None
                
            date_str, team = match.groups()
            game_date = datetime.strptime(date_str, '%Y%m%d')
            
            # Extract title for additional info
            title = soup.find('title')
            title_text = title.get_text() if title else ""
            
            # Determine if it's playoffs
            is_playoffs = "Finals" in title_text or "Conference" in title_text or "Round" in title_text
            
            return {
                'game_id': filename.replace('.html', ''),
                'date': game_date.strftime('%Y-%m-%d'),
                'home_team': team,
                'is_playoffs': is_playoffs,
                'title': title_text
            }
            
        except Exception as e:
            logger.error(f"Error extracting game info from {filename}: {str(e)}")
            return None
    
    def _extract_pbp_data(self, soup: BeautifulSoup) -> List[Dict]:
        """Extract play-by-play data from the HTML table."""
        pbp_data = []
        
        try:
            # Find the play-by-play table
            # Look for table with time-based data
            tables = soup.find_all('table')
            
            for table in tables:
                rows = table.find_all('tr')
                if len(rows) < 10:  # Skip small tables
                    continue
                    
                # Check if this looks like PBP data (has time column)
                first_row = rows[0]
                cells = first_row.find_all(['td', 'th'])
                
                if len(cells) >= 4 and self._is_time_format(cells[0].get_text()):
                    # This looks like PBP data
                    for row in rows[1:]:  # Skip header
                        play_data = self._parse_pbp_row(row)
                        if play_data:
                            pbp_data.append(play_data)
                    break
            
            return pbp_data
            
        except Exception as e:
            logger.error(f"Error extracting PBP data: {str(e)}")
            return []
    
    def _is_time_format(self, text: str) -> bool:
        """Check if text looks like a time format (MM:SS.S)."""
        return bool(re.match(r'\d{1,2}:\d{2}\.\d', text.strip()))
    
    def _parse_pbp_row(self, row) -> Optional[Dict]:
        """Parse a single PBP row."""
        try:
            cells = row.find_all('td')
            if len(cells) < 4:
                return None
            
            # Extract time
            time_text = cells[0].get_text().strip()
            if not self._is_time_format(time_text):
                return None
            
            # Parse time to seconds
            time_seconds = self._time_to_seconds(time_text)
            
            # Extract score
            score_text = cells[3].get_text().strip()
            score = self._parse_score(score_text)
            
            # Extract play description
            play_desc = ""
            if len(cells) >= 6:
                play_desc = cells[5].get_text().strip()
            elif len(cells) >= 2:
                play_desc = cells[1].get_text().strip()
            
            # Determine event type and players
            event_info = self._parse_play_description(play_desc)
            
            return {
                'time': time_text,
                'time_seconds': time_seconds,
                'score': score,
                'play_description': play_desc,
                **event_info
            }
            
        except Exception as e:
            logger.debug(f"Error parsing row: {str(e)}")
            return None
    
    def _time_to_seconds(self, time_text: str) -> float:
        """Convert time string (MM:SS.S) to seconds."""
        try:
            minutes, seconds = time_text.split(':')
            return float(minutes) * 60 + float(seconds)
        except:
            return 0.0
    
    def _parse_score(self, score_text: str) -> Optional[Dict]:
        """Parse score text (e.g., '35-46') to dict."""
        try:
            if '-' in score_text:
                away_score, home_score = score_text.split('-')
                return {
                    'away_score': int(away_score),
                    'home_score': int(home_score),
                    'total_score': int(away_score) + int(home_score)
                }
        except:
            pass
        return None
    
    def _parse_play_description(self, desc: str) -> Dict:
        """Parse play description to extract event type and players."""
        desc_lower = desc.lower()
        
        # Initialize event info
        event_info = {
            'event_type': 'other',
            'player_name': None,
            'assist_player': None,
            'is_scoring_play': False,
            'points': 0,
            'shot_type': None,
            'shot_distance': None,
            'is_made': False
        }
        
        # Check for scoring plays
        if 'makes' in desc_lower:
            event_info['event_type'] = 'scoring'
            event_info['is_scoring_play'] = True
            event_info['is_made'] = True
            
            # Extract points
            if '3-pt' in desc_lower:
                event_info['points'] = 3
                event_info['shot_type'] = '3pt'
            elif 'free throw' in desc_lower:
                event_info['points'] = 1
                event_info['shot_type'] = 'ft'
            else:
                event_info['points'] = 2
                event_info['shot_type'] = '2pt'
                
        elif 'misses' in desc_lower:
            event_info['event_type'] = 'missed_shot'
            event_info['is_made'] = False
            
            if '3-pt' in desc_lower:
                event_info['shot_type'] = '3pt'
            elif 'free throw' in desc_lower:
                event_info['shot_type'] = 'ft'
            else:
                event_info['shot_type'] = '2pt'
        
        # Extract player names
        player_match = re.search(r'([A-Z]\.[A-Za-z]+)', desc)
        if player_match:
            event_info['player_name'] = player_match.group(1)
        
        # Extract assist player
        assist_match = re.search(r'assist by ([A-Z]\.[A-Za-z]+)', desc)
        if assist_match:
            event_info['assist_player'] = assist_match.group(1)
        
        return event_info
    
    def process_all_files(self) -> pd.DataFrame:
        """Process all HTML files and return structured DataFrame."""
        logger.info(f"Processing files from {self.data_dir}")
        
        files = [f for f in os.listdir(self.data_dir) if f.endswith('.html')]
        logger.info(f"Found {len(files)} files to process")
        
        all_data = []
        
        for i, filename in enumerate(files):
            if i % 100 == 0:
                logger.info(f"Processing file {i+1}/{len(files)}: {filename}")
            
            game_data = self.parse_game_file(filename)
            if game_data:
                # Flatten the data for DataFrame
                for play in game_data['pbp_data']:
                    play_data = {
                        'game_id': game_data['game_info']['game_id'],
                        'date': game_data['game_info']['date'],
                        'home_team': game_data['game_info']['home_team'],
                        'is_playoffs': game_data['game_info']['is_playoffs'],
                        **play
                    }
                    all_data.append(play_data)
        
        df = pd.DataFrame(all_data)
        logger.info(f"Processed {len(df)} total plays from {len(files)} games")
        
        return df
    
    def save_processed_data(self, df: pd.DataFrame, output_path: str = "../data/processed/nba_pbp_processed.csv"):
        """Save processed data to CSV file."""
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        df.to_csv(output_path, index=False)
        logger.info(f"Saved processed data to {output_path}")
        
        # Also save summary statistics
        summary = {
            'total_games': df['game_id'].nunique(),
            'total_plays': len(df),
            'date_range': f"{df['date'].min()} to {df['date'].max()}",
            'playoff_games': df[df['is_playoffs']]['game_id'].nunique(),
            'regular_season_games': df[~df['is_playoffs']]['game_id'].nunique(),
            'scoring_plays': len(df[df['is_scoring_play']]),
            'unique_players': df['player_name'].nunique()
        }
        
        summary_path = output_path.replace('.csv', '_summary.json')
        with open(summary_path, 'w') as f:
            json.dump(summary, f, indent=2)
        logger.info(f"Saved summary to {summary_path}")

def main():
    """Main function to run the data preprocessing."""
    processor = NBAPBPProcessor()
    
    # Process all files
    df = processor.process_all_files()
    
    # Save processed data
    processor.save_processed_data(df)
    
    # Print summary
    print("\n=== Data Processing Summary ===")
    print(f"Total games processed: {df['game_id'].nunique()}")
    print(f"Total plays: {len(df)}")
    print(f"Date range: {df['date'].min()} to {df['date'].max()}")
    print(f"Playoff games: {df[df['is_playoffs']]['game_id'].nunique()}")
    print(f"Regular season games: {df[~df['is_playoffs']]['game_id'].nunique()}")
    print(f"Scoring plays: {len(df[df['is_scoring_play']])}")
    print(f"Unique players: {df['player_name'].nunique()}")

if __name__ == "__main__":
    main()
```

이제 필요한 의존성을 설치하기 위한 requirements.txt 파일을 생성하겠습니다.


```pip-requirements
pandas>=1.5.0
beautifulsoup4>=4.11.0
lxml>=4.9.0
numpy>=1.21.0
scikit-learn>=1.1.0
matplotlib>=3.5.0
seaborn>=0.11.0
jupyter>=1.0.0
```

이제 의존성을 설치하겠습니다.


좋습니다! 이제 데이터 전처리 스크립트를 실행해보겠습니다. 먼저 테스트로 몇 개 파일만 처리해보겠습니다.


```python
#!/usr/bin/env python3
"""
Test script for NBA PBP data preprocessing
"""

import os
import sys
from data_preprocessing import NBAPBPProcessor

def test_single_file():
    """Test processing a single file."""
    processor = NBAPBPProcessor()
    
    # Test with one file
    test_file = "202406170BOS.html"  # Finals game
    print(f"Testing with file: {test_file}")
    
    game_data = processor.parse_game_file(test_file)
    
    if game_data:
        print(f"✓ Successfully parsed {test_file}")
        print(f"  Game info: {game_data['game_info']}")
        print(f"  Number of plays: {len(game_data['pbp_data'])}")
        
        # Show first few plays
        print("\nFirst 5 plays:")
        for i, play in enumerate(game_data['pbp_data'][:5]):
            print(f"  {i+1}. {play['time']} - {play['play_description']}")
            
        return True
    else:
        print(f"✗ Failed to parse {test_file}")
        return False

def test_multiple_files():
    """Test processing multiple files."""
    processor = NBAPBPProcessor()
    
    # Get list of files
    files = [f for f in os.listdir(processor.data_dir) if f.endswith('.html')]
    print(f"Found {len(files)} files to test")
    
    # Test first 5 files
    test_files = files[:5]
    successful = 0
    
    for filename in test_files:
        print(f"\nTesting: {filename}")
        game_data = processor.parse_game_file(filename)
        
        if game_data:
            print(f"  ✓ Success - {len(game_data['pbp_data'])} plays")
            successful += 1
        else:
            print(f"  ✗ Failed")
    
    print(f"\nTest Results: {successful}/{len(test_files)} files processed successfully")
    return successful == len(test_files)

if __name__ == "__main__":
    print("=== NBA PBP Data Preprocessing Test ===\n")
    
    # Test single file
    print("1. Testing single file processing...")
    single_success = test_single_file()
    
    print("\n" + "="*50 + "\n")
    
    # Test multiple files
    print("2. Testing multiple file processing...")
    multi_success = test_multiple_files()
    
    print("\n" + "="*50)
    print("SUMMARY:")
    print(f"Single file test: {'✓ PASSED' if single_success else '✗ FAILED'}")
    print(f"Multiple files test: {'✓ PASSED' if multi_success else '✗ FAILED'}")
    
    if single_success and multi_success:
        print("\n🎉 All tests passed! Ready to process full dataset.")
    else:
        print("\n❌ Some tests failed. Check the code.")
```

이제 테스트를 실행해보겠습니다.


파싱이 실패했네요. HTML 구조를 더 자세히 살펴보고 파싱 로직을 수정해야겠습니다. 먼저 실제 HTML 구조를 확인해보겠습니다.


이제 HTML 구조를 파악했습니다. 테이블 헤더를 찾아보겠습니다.


이제 테이블 헤더를 찾았습니다. PBP 테이블의 헤더를 확인해보겠습니다.


완벽합니다! 이제 HTML 구조를 파악했습니다. 파싱 로직을 수정하겠습니다.


```python
    def _extract_pbp_data(self, soup: BeautifulSoup) -> List[Dict]:
        """Extract play-by-play data from the HTML table."""
        pbp_data = []
        
        try:
            # Find the play-by-play table
            # Look for table with time-based data
            tables = soup.find_all('table')
            
            for table in tables:
                rows = table.find_all('tr')
                if len(rows) < 10:  # Skip small tables
                    continue
                    
                # Check if this looks like PBP data (has time column)
                first_row = rows[0]
                cells = first_row.find_all(['td', 'th'])
                
                if len(cells) >= 4 and self._is_time_format(cells[0].get_text()):
                    # This looks like PBP data
                    for row in rows[1:]:  # Skip header
                        play_data = self._parse_pbp_row(row)
                        if play_data:
                            pbp_data.append(play_data)
                    break
            
            return pbp_data
            
        except Exception as e:
            logger.error(f"Error extracting PBP data: {str(e)}")
            return []

->

    def _extract_pbp_data(self, soup: BeautifulSoup) -> List[Dict]:
        """Extract play-by-play data from the HTML table."""
        pbp_data = []
        
        try:
            # Find the play-by-play table by ID
            pbp_table = soup.find('table', {'id': 'pbp'})
            if not pbp_table:
                logger.warning("Could not find PBP table with id='pbp'")
                return []
            
            # Get all rows from the table
            rows = pbp_table.find_all('tr')
            
            for row in rows:
                # Skip header rows
                if 'thead' in row.get('class', []):
                    continue
                
                # Parse the row
                play_data = self._parse_pbp_row(row)
                if play_data:
                    pbp_data.append(play_data)
            
            return pbp_data
            
        except Exception as e:
            logger.error(f"Error extracting PBP data: {str(e)}")
            return []
```

이제 PBP 행 파싱 로직도 수정해야 합니다. HTML 구조에 맞게 수정하겠습니다.


```python
    def _parse_pbp_row(self, row) -> Optional[Dict]:
        """Parse a single PBP row."""
        try:
            cells = row.find_all('td')
            if len(cells) < 4:
                return None
            
            # Extract time
            time_text = cells[0].get_text().strip()
            if not self._is_time_format(time_text):
                return None
            
            # Parse time to seconds
            time_seconds = self._time_to_seconds(time_text)
            
            # Extract score
            score_text = cells[3].get_text().strip()
            score = self._parse_score(score_text)
            
            # Extract play description
            play_desc = ""
            if len(cells) >= 6:
                play_desc = cells[5].get_text().strip()
            elif len(cells) >= 2:
                play_desc = cells[1].get_text().strip()
            
            # Determine event type and players
            event_info = self._parse_play_description(play_desc)
            
            return {
                'time': time_text,
                'time_seconds': time_seconds,
                'score': score,
                'play_description': play_desc,
                **event_info
            }
            
        except Exception as e:
            logger.debug(f"Error parsing row: {str(e)}")
            return None

->

    def _parse_pbp_row(self, row) -> Optional[Dict]:
        """Parse a single PBP row."""
        try:
            cells = row.find_all('td')
            if len(cells) < 4:
                return None
            
            # Extract time
            time_text = cells[0].get_text().strip()
            if not self._is_time_format(time_text):
                return None
            
            # Parse time to seconds
            time_seconds = self._time_to_seconds(time_text)
            
            # Extract score (column 3)
            score_text = cells[3].get_text().strip()
            score = self._parse_score(score_text)
            
            # Extract play description from appropriate column
            play_desc = ""
            if len(cells) >= 6:
                # Check if there's content in column 5 (Boston plays)
                boston_play = cells[5].get_text().strip()
                dallas_play = cells[1].get_text().strip()
                
                if boston_play and boston_play != "&nbsp;":
                    play_desc = boston_play
                elif dallas_play and dallas_play != "&nbsp;":
                    play_desc = dallas_play
                else:
                    # Check for special cases like jump ball
                    for i, cell in enumerate(cells):
                        cell_text = cell.get_text().strip()
                        if cell_text and cell_text != "&nbsp;" and i > 0:
                            play_desc = cell_text
                            break
            elif len(cells) >= 2:
                play_desc = cells[1].get_text().strip()
            
            # Determine event type and players
            event_info = self._parse_play_description(play_desc)
            
            return {
                'time': time_text,
                'time_seconds': time_seconds,
                'score': score,
                'play_description': play_desc,
                **event_info
            }
            
        except Exception as e:
            logger.debug(f"Error parsing row: {str(e)}")
            return None
```

이제 테스트를 다시 실행해보겠습니다.


여전히 실패하고 있네요. 디버깅을 위해 더 자세한 로그를 추가해보겠습니다.


```python
#!/usr/bin/env python3
"""
Debug script for NBA PBP data preprocessing
"""

import os
from bs4 import BeautifulSoup

def debug_single_file():
    """Debug a single file to understand the structure."""
    data_dir = "../data/raw/nba_bbr"
    test_file = "202406170BOS.html"
    filepath = os.path.join(data_dir, test_file)
    
    print(f"Debugging file: {filepath}")
    
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        
        soup = BeautifulSoup(content, 'html.parser')
        
        # Look for PBP table
        pbp_table = soup.find('table', {'id': 'pbp'})
        if pbp_table:
            print("✓ Found PBP table with id='pbp'")
            
            # Count rows
            rows = pbp_table.find_all('tr')
            print(f"  Total rows: {len(rows)}")
            
            # Show first few rows
            print("\nFirst 10 rows:")
            for i, row in enumerate(rows[:10]):
                cells = row.find_all(['td', 'th'])
                cell_texts = [cell.get_text().strip() for cell in cells]
                print(f"  Row {i}: {cell_texts}")
                
        else:
            print("✗ Could not find PBP table with id='pbp'")
            
            # Look for any table
            tables = soup.find_all('table')
            print(f"Found {len(tables)} tables")
            
            for i, table in enumerate(tables):
                table_id = table.get('id', 'no-id')
                table_class = table.get('class', 'no-class')
                print(f"  Table {i}: id='{table_id}', class='{table_class}'")
                
                # Check if this table has time-like data
                rows = table.find_all('tr')
                if rows:
                    first_row = rows[0]
                    cells = first_row.find_all(['td', 'th'])
                    if cells:
                        first_cell = cells[0].get_text().strip()
                        print(f"    First cell: '{first_cell}'")
                        
                        # Check if it looks like time
                        import re
                        if re.match(r'\d{1,2}:\d{2}\.\d', first_cell):
                            print(f"    ✓ This looks like a PBP table!")
                            print(f"    Total rows: {len(rows)}")
                            
                            # Show first few rows
                            print("    First 5 rows:")
                            for j, row in enumerate(rows[:5]):
                                cells = row.find_all(['td', 'th'])
                                cell_texts = [cell.get_text().strip() for cell in cells]
                                print(f"      Row {j}: {cell_texts}")
                            break
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    debug_single_file()
```

좋습니다! 이제 테이블 구조를 파악했습니다. 문제는 `_parse_pbp_row` 함수에서 시간 형식을 확인하는 부분에 있습니다. 수정해보겠습니다.


```python
    def _is_time_format(self, text: str) -> bool:
        """Check if text looks like a time format (MM:SS.S)."""
        return bool(re.match(r'\d{1,2}:\d{2}\.\d', text.strip()))

->

    def _is_time_format(self, text: str) -> bool:
        """Check if text looks like a time format (MM:SS.S)."""
        text = text.strip()
        # Check for time format like "12:00.0" or "1:23.0"
        return bool(re.match(r'^\d{1,2}:\d{2}\.\d$', text))
```

이제 테스트를 다시 실행해보겠습니다.


여전히 실패하고 있습니다. 더 자세한 디버깅을 위해 로그를 추가해보겠습니다.


```python
#!/usr/bin/env python3
"""
Detailed debug script for NBA PBP data preprocessing
"""

import os
import re
from bs4 import BeautifulSoup
from datetime import datetime

def debug_parsing():
    """Debug the parsing process step by step."""
    data_dir = "../data/raw/nba_bbr"
    test_file = "202406170BOS.html"
    filepath = os.path.join(data_dir, test_file)
    
    print(f"Debugging file: {filepath}")
    
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        
        soup = BeautifulSoup(content, 'html.parser')
        
        # Test game info extraction
        print("\n1. Testing game info extraction...")
        match = re.match(r'(\d{8})([A-Z]{3})\.html', test_file)
        if match:
            date_str, team = match.groups()
            print(f"  Date: {date_str}, Team: {team}")
            
            game_date = datetime.strptime(date_str, '%Y%m%d')
            print(f"  Parsed date: {game_date}")
            
            title = soup.find('title')
            title_text = title.get_text() if title else ""
            print(f"  Title: {title_text}")
            
            is_playoffs = "Finals" in title_text or "Conference" in title_text or "Round" in title_text
            print(f"  Is playoffs: {is_playoffs}")
        else:
            print("  ✗ Failed to parse filename")
        
        # Test PBP table extraction
        print("\n2. Testing PBP table extraction...")
        pbp_table = soup.find('table', {'id': 'pbp'})
        if pbp_table:
            print("  ✓ Found PBP table")
            
            rows = pbp_table.find_all('tr')
            print(f"  Total rows: {len(rows)}")
            
            # Test row parsing
            print("\n3. Testing row parsing...")
            successful_plays = 0
            
            for i, row in enumerate(rows):
                if 'thead' in row.get('class', []):
                    continue
                
                cells = row.find_all('td')
                if len(cells) < 4:
                    continue
                
                # Extract time
                time_text = cells[0].get_text().strip()
                if not re.match(r'^\d{1,2}:\d{2}\.\d$', time_text):
                    continue
                
                # Extract score
                score_text = cells[3].get_text().strip()
                
                # Extract play description
                play_desc = ""
                if len(cells) >= 6:
                    boston_play = cells[5].get_text().strip()
                    dallas_play = cells[1].get_text().strip()
                    
                    if boston_play and boston_play != "&nbsp;":
                        play_desc = boston_play
                    elif dallas_play and dallas_play != "&nbsp;":
                        play_desc = dallas_play
                    else:
                        for j, cell in enumerate(cells):
                            cell_text = cell.get_text().strip()
                            if cell_text and cell_text != "&nbsp;" and j > 0:
                                play_desc = cell_text
                                break
                elif len(cells) >= 2:
                    play_desc = cells[1].get_text().strip()
                
                if play_desc:
                    successful_plays += 1
                    if successful_plays <= 5:
                        print(f"    Play {successful_plays}: {time_text} - {play_desc}")
            
            print(f"  Successfully parsed {successful_plays} plays")
            
        else:
            print("  ✗ Could not find PBP table")
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    debug_parsing()
```

좋습니다! 파싱은 잘 되고 있는데, 게임 정보 추출에서 문제가 있네요. 파일명 파싱을 수정하겠습니다.


```python
    def _extract_game_info(self, soup: BeautifulSoup, filename: str) -> Optional[Dict]:
        """Extract basic game information."""
        try:
            # Parse filename to get date and teams
            # Format: YYYYMMDDTEAM.html
            match = re.match(r'(\d{8})([A-Z]{3})\.html', filename)
            if not match:
                return None
                
            date_str, team = match.groups()
            game_date = datetime.strptime(date_str, '%Y%m%d')
            
            # Extract title for additional info
            title = soup.find('title')
            title_text = title.get_text() if title else ""
            
            # Determine if it's playoffs
            is_playoffs = "Finals" in title_text or "Conference" in title_text or "Round" in title_text
            
            return {
                'game_id': filename.replace('.html', ''),
                'date': game_date.strftime('%Y-%m-%d'),
                'home_team': team,
                'is_playoffs': is_playoffs,
                'title': title_text
            }
            
        except Exception as e:
            logger.error(f"Error extracting game info from {filename}: {str(e)}")
            return None

->

    def _extract_game_info(self, soup: BeautifulSoup, filename: str) -> Optional[Dict]:
        """Extract basic game information."""
        try:
            # Parse filename to get date and teams
            # Format: YYYYMMDDTEAM.html
            match = re.match(r'(\d{8})([A-Z]{3})\.html', filename)
            if not match:
                logger.warning(f"Could not parse filename: {filename}")
                return None
                
            date_str, team = match.groups()
            game_date = datetime.strptime(date_str, '%Y%m%d')
            
            # Extract title for additional info
            title = soup.find('title')
            title_text = title.get_text() if title else ""
            
            # Determine if it's playoffs
            is_playoffs = "Finals" in title_text or "Conference" in title_text or "Round" in title_text
            
            return {
                'game_id': filename.replace('.html', ''),
                'date': game_date.strftime('%Y-%m-%d'),
                'home_team': team,
                'is_playoffs': is_playoffs,
                'title': title_text
            }
            
        except Exception as e:
            logger.error(f"Error extracting game info from {filename}: {str(e)}")
            return None
```

이제 테스트를 다시 실행해보겠습니다.


파일명 파싱에 문제가 있네요. 실제 파일명을 확인해보겠습니다.


아, 파일명이 다르네요! 실제 파일명은 `202310240DEN.html` 형식인데, 테스트에서는 `202406170BOS.html`을 사용하고 있었습니다. 파일명 파싱을 수정하겠습니다.


```python
            # Parse filename to get date and teams
            # Format: YYYYMMDDTEAM.html
            match = re.match(r'(\d{8})([A-Z]{3})\.html', filename)

->

            # Parse filename to get date and teams
            # Format: YYYYMMDDTEAM.html (e.g., 202310240DEN.html)
            match = re.match(r'(\d{8})([A-Z]{3})\.html', filename)
```

이제 실제 파일로 테스트해보겠습니다.


```python
#!/usr/bin/env python3
"""
Test script for NBA PBP data preprocessing
"""

import os
import sys
from data_preprocessing import NBAPBPProcessor

def test_single_file():
    """Test processing a single file."""
    processor = NBAPBPProcessor()
    
    # Test with one file (use actual filename)
    test_file = "202310240DEN.html"  # Regular season game
    print(f"Testing with file: {test_file}")
    
    game_data = processor.parse_game_file(test_file)
    
    if game_data:
        print(f"✓ Successfully parsed {test_file}")
        print(f"  Game info: {game_data['game_info']}")
        print(f"  Number of plays: {len(game_data['pbp_data'])}")
        
        # Show first few plays
        print("\nFirst 5 plays:")
        for i, play in enumerate(game_data['pbp_data'][:5]):
            print(f"  {i+1}. {play['time']} - {play['play_description']}")
            
        return True
    else:
        print(f"✗ Failed to parse {test_file}")
        return False

def test_multiple_files():
    """Test processing multiple files."""
    processor = NBAPBPProcessor()
    
    # Get list of files
    files = [f for f in os.listdir(processor.data_dir) if f.endswith('.html')]
    print(f"Found {len(files)} files to test")
    
    # Test first 5 files
    test_files = files[:5]
    successful = 0
    
    for filename in test_files:
        print(f"\nTesting: {filename}")
        game_data = processor.parse_game_file(filename)
        
        if game_data:
            print(f"  ✓ Success - {len(game_data['pbp_data'])} plays")
            successful += 1
        else:
            print(f"  ✗ Failed")
    
    print(f"\nTest Results: {successful}/{len(test_files)} files processed successfully")
    return successful == len(test_files)

if __name__ == "__main__":
    print("=== NBA PBP Data Preprocessing Test ===\n")
    
    # Test single file
    print("1. Testing single file processing...")
    single_success = test_single_file()
    
    print("\n" + "="*50 + "\n")
    
    # Test multiple files
    print("2. Testing multiple file processing...")
    multi_success = test_multiple_files()
    
    print("\n" + "="*50)
    print("SUMMARY:")
    print(f"Single file test: {'✓ PASSED' if single_success else '✗ FAILED'}")
    print(f"Multiple files test: {'✓ PASSED' if multi_success else '✗ FAILED'}")
    
    if single_success and multi_success:
        print("\n🎉 All tests passed! Ready to process full dataset.")
    else:
        print("\n❌ Some tests failed. Check the code.")
```

여전히 파일명 파싱이 실패하고 있습니다. 정규식을 디버깅해보겠습니다.


```python
#!/usr/bin/env python3
"""
Debug filename parsing
"""

import re

def test_filename_parsing():
    """Test filename parsing with various examples."""
    test_filenames = [
        "202310240DEN.html",
        "202402130BRK.html", 
        "202311110ATL.html",
        "202404110POR.html",
        "202310280CLE.html",
        "202401050PHI.html"
    ]
    
    pattern = r'(\d{8})([A-Z]{3})\.html'
    
    print("Testing filename parsing:")
    print(f"Pattern: {pattern}")
    print()
    
    for filename in test_filenames:
        match = re.match(pattern, filename)
        if match:
            date_str, team = match.groups()
            print(f"✓ {filename} -> Date: {date_str}, Team: {team}")
        else:
            print(f"✗ {filename} -> No match")
            
        # Also test without .html
        filename_no_ext = filename.replace('.html', '')
        match_no_ext = re.match(r'(\d{8})([A-Z]{3})', filename_no_ext)
        if match_no_ext:
            date_str, team = match_no_ext.groups()
            print(f"  (without .html) ✓ {filename_no_ext} -> Date: {date_str}, Team: {team}")
        else:
            print(f"  (without .html) ✗ {filename_no_ext} -> No match")

if __name__ == "__main__":
    test_filename_parsing()
```

정규식이 매치되지 않고 있습니다. 파일명을 다시 확인해보겠습니다.


파일명은 맞는데 정규식이 작동하지 않네요. 더 자세히 디버깅해보겠습니다.


```python
#!/usr/bin/env python3
"""
Debug filename parsing
"""

import re

def test_filename_parsing():
    """Test filename parsing with various examples."""
    test_filenames = [
        "202310240DEN.html",
        "202402130BRK.html", 
        "202311110ATL.html",
        "202404110POR.html",
        "202310280CLE.html",
        "202401050PHI.html"
    ]
    
    pattern = r'(\d{8})([A-Z]{3})\.html'
    
    print("Testing filename parsing:")
    print(f"Pattern: {pattern}")
    print()
    
    for filename in test_filenames:
        print(f"Testing: '{filename}'")
        print(f"  Length: {len(filename)}")
        print(f"  Characters: {[c for c in filename]}")
        
        match = re.match(pattern, filename)
        if match:
            date_str, team = match.groups()
            print(f"  ✓ Match: Date: {date_str}, Team: {team}")
        else:
            print(f"  ✗ No match")
            
            # Try different patterns
            patterns_to_try = [
                r'(\d{8})([A-Z]{3})\.html',
                r'(\d{8})([A-Z]{3})',
                r'(\d{8})([A-Z]+)\.html',
                r'(\d{8})([A-Z]+)'
            ]
            
            for i, p in enumerate(patterns_to_try):
                m = re.match(p, filename)
                if m:
                    print(f"    Pattern {i+1} ({p}) matches: {m.groups()}")
                else:
                    print(f"    Pattern {i+1} ({p}) no match")
        
        print()

if __name__ == "__main__":
    test_filename_parsing()
```

이상하네요. 정규식이 작동하지 않고 있습니다. 다른 방법으로 테스트해보겠습니다.


정규식이 작동하지 않네요. 다른 방법으로 테스트해보겠습니다.


정규식에 문제가 있는 것 같습니다. 더 간단한 방법으로 테스트해보겠습니다.


숫자 부분은 작동합니다. 이제 팀 코드 부분을 테스트해보겠습니다.


