[25.11.29] 뒤늦게 올리는 주말 TIL

최다빈·2025년 11월 30일

주말

목록 보기
3/4

🏀 NBA Full Data Engineering & Analysis TIL (1)


📌 0. 데이터셋 구성

  • games.csv (경기별 데이터)
  • games_details.csv (선수별 경기 기록)
  • players.csv (선수 정보)
  • teams.csv (팀 정보)

NBA 공홈 기록 기반 대규모(7GB) 플레이 바이 플레이 데이터.


📌 1. 전체 전처리 파이프라인 개요

오늘 최종적으로 정리된 파이프라인은 아래 순서대로 이뤄졌다.

  1. 파일 로드
  2. games_details + games 병합
  3. players 병합(PLAYER_NAME 충돌 해결)
  4. teams 병합(TEAM_CITY/ABBR 중복 해결)
  5. MIN(MM:SS) → float 변환
  6. 중복 컬럼 제거(Index.duplicated 활용)
  7. 결측치 처리
  8. 타입 변환(datetime)
  9. 이상치 제거
  10. 유효 기록 필터링
  11. 통계 컬럼 추가(PPM, SEI, SEI/M)
  12. 선수별 SEI 통계
  13. 관련성 분석
  14. 승·패 패턴 분석

📌 2. 파일 로드

import pandas as pd
import numpy as np

print("전체 프로세스 시작")

# 1. 파일 로드
try:
    df_games = pd.read_csv('games.csv')
    df_details = pd.read_csv('games_details.csv')
    df_players = pd.read_csv('players.csv')
    df_teams = pd.read_csv('teams.csv') # ranking은 안 씀
    print("파일 로드 성공")
except FileNotFoundError:
    print("실패! 파일 경로 확인.")

과정 & 문제

  • games_details.csv 사이즈 때문에 로딩만 10초~15초 걸림
  • dtype warning이 어떤 이유로 뜨는 건지 어떻게 해결해야 하는지 몰랐음..
  • 근데 그냥 무시해도 되는 경고였다는 걸 뒤늦게 깨달았음.

📌 3. 1차 병합 — games_details + games

  • 경기 기록에 SEASON, GAME_DATE_EST, HOME_TEAM_ID, VISITOR_TEAM_ID 붙이기
merged_df = pd.merge(
    df_details, 
    df_games[['GAME_ID', 'SEASON', 'GAME_DATE_EST', 'HOME_TEAM_ID', 'VISITOR_TEAM_ID']], 
    on='GAME_ID', 
    how='left'
)
print(f"1차 병합 완료 (행 개수: {len(merged_df)})")

병합 후 형태:

(3,218,444 rows, 29 columns) → (3,218,444 rows, 34 columns)

❗ 느낀 점

  • 데이터 행 수가 변하지 않음.
  • 병합 기준 설정 잘 함.

■ 핵심 포인트

  • 기준: GAME_ID
  • games_details가 record 단위, games는 경기 단위 → 정상
  • 오류 X

📌 4. 2차 병합 — players(PLAYER_NAME 충돌 해결)

가장 오래 걸렸던 부분이다..

병합 후 컬럼 상태:

PLAYER_NAME_x
PLAYER_NAME_y

둘 다 PLAYER_NAME인데 Whyrano,,,,
→ games_details는 경기 시점 이름, players는 DB 기반 공식 이름.

그래서 suffix 처리 후 최신 이름으로 통일.

merged_df = pd.merge(
    merged_df, 
    df_players[['PLAYER_ID', 'PLAYER_NAME']], 
    on='PLAYER_ID', 
    how='left',
    suffixes=('_OLD', '_NEW')
)

# PLAYER_NAME_NEW가 진짜 최신 컬럼
if 'PLAYER_NAME_NEW' in merged_df.columns:
    merged_df['PLAYER_NAME'] = merged_df['PLAYER_NAME_NEW']
    merged_df.drop(columns=['PLAYER_NAME_NEW', 'PLAYER_NAME_OLD'], errors='ignore', inplace=True)

print("선수 정보 병합 완료")

🐞 NameError / KeyError 관련 문제 발생했던 구간

  • notebook 런타임 재시작하면 merged_df가 없어짐 → NameError
  • 해결: 전처리 파이프라인을 한 셀에 몰지 않고 단계별 셀 실행
  • PLAYER_NAME 컬럼 충돌로 KeyError 발생
  • suffix 설정 후 OLD/NEW 정리로 해결

5. 팀 정보 병합 — TEAM_CITY/ABBR 중복 해결

갯수는 맞는데, 중복 컬럼이 6개씩 튀어나옴..

  • TEAM_CITY_x
  • TEAM_CITY_y
  • TEAM_CITY_NEW
  • TEAM_CITY_OLD
  • ABBREVIATION_x
  • ABBREVIATION_y
    먼데 이래 많이 튀나와..?

원인: games_details 안에도 TEAM_ID가 있고,
teams 안에도 TEAM_ID가 있어서 모든 조합이 싹 다 생성됨.
수동 정리 해야 함 ㅋㅋㅠㅠㅠㅠ

merged_df = pd.merge(
    merged_df, 
    df_teams[['TEAM_ID', 'ABBREVIATION', 'CITY']],
    on='TEAM_ID', 
    how='left',
    suffixes=('', '_TEAM_INFO')
)

merged_df = merged_df.rename(columns={
    'ABBREVIATION': 'TEAM_ABBREVIATION',
    'CITY': 'TEAM_CITY'
})

print("팀 정보 병합 완료")

🐞 KeyError: TEAM_ID
원인:

  • 일부 단계에서 TEAM_ID가 사라졌음
  • merge 과정 확인 필요

🐞 TEAM_CITY / TEAM_ABBR 중복 4개 생성

해결 코드 ↓

# 중복 컬럼 인덱스 처리
clean_cols = merged_df.columns[~merged_df.columns.duplicated(keep=False)].tolist()
cols = merged_df.columns

abbr_locs = [i for i, col in enumerate(cols) if col == 'TEAM_ABBREVIATION']
city_locs = [i for i, col in enumerate(cols) if col == 'TEAM_CITY']

merged_df_final = merged_df[clean_cols].copy()
merged_df_final['TEAM_ABBREVIATION'] = merged_df.iloc[:, abbr_locs[-1]]
merged_df_final['TEAM_CITY'] = merged_df.iloc[:, city_locs[-1]]

merged_df = merged_df_final

print("중복 컬럼 정리 완료")

느낀 점

  • 그만할까 ㅎㅎㅎㅎ 생각했던 부분임.
  • 팀 메타데ㅣ터가 이래 더럽게 병합될 줄 몰랐음.

📌 6. MIN(MM:SS → float) 변환

🐞 가장 시간을 많이 잡아먹은 지옥 구간

  • 문자열, 공백, NaN, '0', '0:00', '5:7', '_', 'NAN' 같은 값 혼재
  • split 오류
  • int 변환 오류
  • float 변환 오류
    변수가 너무 많아서 함수 뜯어 고치기를 반복함.

최종 확정 함수 ↓

def convert_min_to_float(min_str):
    if pd.isna(min_str):
        return np.nan

    min_str = str(min_str).strip()

    if min_str in ['', '--', '-', '—']:
        return np.nan

    if ':' in min_str:
        try:
            m, s = min_str.split(':')
            return int(m) + int(s) / 60
        except:
            return np.nan

    try:
        return float(min_str)
    except:
        return np.nan

merged['MIN'] = merged['MIN'].apply(convert_min_to_float)

실제로 터진 에러 로그

ValueError: invalid literal for int() with base 10: '—'
ValueError: could not convert string to float: '0:00'
TypeError: argument of type 'float' is not iterable

이거 해결하느라 시간 많이 쏟음 ㅎㅎ,,


📌 7. 결측치 처리 (수치형 = 0, 범주형 = UNKNOWN)

numerical_cols = [
    'MIN','FGM','FGA','FG_PCT','FG3M','FG3A','FG3_PCT','FTM','FTA','FT_PCT',
    'OREB','DREB','REB','AST','STL','BLK','TO','PF','PTS','PLUS_MINUS'
]

merged_df[numerical_cols] = merged_df[numerical_cols].fillna(0)

categorical_cols = [
    'PLAYER_NAME','TEAM_ABBREVIATION','TEAM_CITY','NICKNAME'
]

for col in [c for c in categorical_cols if c in merged_df.columns]:
    merged_df[col] = merged_df[col].fillna('UNKNOWN')

print("결측치 처리 완료")

📌 8. 날짜 형식 변환

merged_df['GAME_DATE_EST'] = pd.to_datetime(merged_df['GAME_DATE_EST'])
print("GAME_DATE_EST 타입:", merged_df['GAME_DATE_EST'].dtype)
print("날짜 형식 변환 완료")

📌 9. 이상치 제거

9-1. PLUS_MINUS 결측 제거

merged_df.dropna(subset=['PLUS_MINUS'], inplace=True)

9-2. MIN < 0 제거

merged_df = merged_df[merged_df['MIN'] >= 0].copy()

9-3. 유효 기록 기준 필터링

merged_df = merged_df[
    (merged_df['MIN'] > 0) &
    ((merged_df['FGA'] > 0) | (merged_df['FTA'] > 0) | (merged_df['REB'] > 0))
].copy()

print("유효 기록 필터링 완료")

👉 결과

필터 전: 3,218,444  
필터 후: 2,792,310  

42만개 행 제거


📌 10. 통계 생성 — PPM / SEI / SEI per MIN

10-1. PPM

merged_df['PPM'] = merged_df['PTS'] / merged_df['MIN']
print("PPM 생성 완료")

10-2. SEI 종합 효율 지표
수식:
**SEI = (PTS + REB + AST + STL + BLK) − (TO + (FGA − FGM))**

merged_df['SEI'] = (
    merged_df['PTS'] + merged_df['REB'] + merged_df['AST'] +
    merged_df['STL'] + merged_df['BLK']
) - (
    merged_df['TO'] + (merged_df['FGA'] - merged_df['FGM'])
)

merged_df['SEI_Per_Minute'] = merged_df['SEI'] / merged_df['MIN']

print("SEI 및 분당 효율 생성 완료")

📌 11. 선수별 퍼포먼스 (SEI/M) 분석

player_sei_stats = merged_df.groupby('PLAYER_NAME').agg(
    total_minutes=('MIN','sum'),
    avg_sei_per_min=('SEI_Per_Minute','mean'),
    total_pts=('PTS','sum')
)

top_sei_players = player_sei_stats[player_sei_stats['total_minutes'] >= 1000]
top_sei_players = top_sei_players.sort_values(by='avg_sei_per_min', ascending=False)

print(top_sei_players.head(10))

출력 결과 ⬇️

PLAYER_NAMEtotal_minutestotal_ptsavg_sei_per_min
Anthony Bennett1405951901.128
Johnathan Motley14298581.088
Joel Embiid77624640200.967
Nikola Jokic98088641550.935
Mario West30186390.915
Luka Doncic22656180300.889
Zion Williamson391731010.882
Giannis Antetokounmpo1824541249150.880
Anthony Davis1963111373440.862
Jeff Withey1162038650.852

📌 12. 점수차 영향 요인 분석 (상관관계)

correlation_cols = [
    'PLUS_MINUS','MIN','PTS','REB','AST','STL','BLK','TO',
    'FGM','FGA','FG_PCT','FG3M','FG3A','FTM','FTA'
]

corr = merged_df[correlation_cols].corr()
plus_minus_corr = corr['PLUS_MINUS'].sort_values(ascending=False)
print(plus_minus_corr)

주요 결과

  • PTS, FGM, FG_PCT가 상관 높음.
  • TURNOVER는 음수 상관.
  • 리바운드 영향 낮음.

📌 13. 홈팀 승리 패턴 분석

home_team_records = merged_df[merged_df['HOME_TEAM_ID'] == merged_df['TEAM_ID']]

win_stats = home_team_records[home_team_records['PLUS_MINUS'] > 0][['PTS','AST','REB','FG_PCT']].mean()
loss_stats = home_team_records[home_team_records['PLUS_MINUS'] <= 0][['PTS','AST','REB','FG_PCT']].mean()

print("승리 시 평균:", win_stats)
print("패배 시 평균:", loss_stats)

승리 시

  • PTS +11 증가
  • FG_PCT +5%
  • AST +3 증가
    홈팀이 이길 땐 공격 효율이 확연히 올라간다는 인사이트.

패턴

  • 승리 시 PTS, AST 모두 상승
  • FG_PCT 차이 상당

📌 14. 디버깅 모음

🐞 NameError: merged_df is not defined
원인: 런타임 초기화
해결: 단계 분리 실행

🐞 KeyError
원인: suffix 충돌 / 컬럼명 다름
해결: rename + duplicated 인덱스 처리

🐞 SettingWithCopyWarning
해결: .loc 사용

🐞 MIN 변환 시 ValueError
해결: robust 변환 함수 제작


11/30 과정

  • 파일 단위 통합
  • 병합 중심
  • 컬럼 충돌 해결
  • 데이터 형태 통일
  • 기본적 결측치 처리
  • 날짜 변환, 타입변환 같은 기본 가공
  • 대표적 이상치 제거
  • 간단 파생 변수 생성

11/31 과정:

  • 데이터 타입 전체 재점검
  • 다른 컬럼 유효성 교차 검증
  • 복잡한 object 👉 숫자 변환 일괄 처리
  • 퍼센티지 재계산
  • 여러 시즌에 걸친 선수/팀 이름 표준화
  • 중복 행 최종 제거
  • 모든 지표의 논리적 이상치 제거
  • 고급 파생 변수(eFG%, TS%, USG%) 생성
  • snake_case 컬럼 표준화
    (TIL 작성 힘듦 이슈로 오늘 공부 끝나면 업로드 예정)

앞으로의 계획은? (~12.14)

  • 데이터 검증 단계
  • 시간 기반 분석 (시계열 분석)
  • 시각화
  • GitHub 정리 / PPT 제작해보기

마무리하며

글이고 작곡이고 다 필요 없고 플젝에만 집중하자 생각하고
지금 이 시간까지 TIL 작성하다 잡니다.
진작 좀 정신 차리면 어디 덧나는지,,~

내배캠,, 좋지만, 너무나도 가혹합니다.

profile
Running on hopes and tiny skills...

0개의 댓글