본문으로 건너뛰기
데이터 분석 · AI 기초집LESSON 22

최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성

난이도초급
예상 시간35분
선수지식이전 강의

22강. 최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성

1. 이번 강의에서 해결할 문제​

분석 코드, 그래프와 모델 점수가 흩어져 있으면 어떤 데이터와 규칙으로 결론이 나왔는지 검토하기 어렵습니다. 입력부터 제안과 한계까지 한 번에 재현해야 합니다.

2. 학습 목표​

3. 핵심 개념​

최종 결과물은 높은 모델 점수가 아니라 재현 가능한 근거 묶음입니다. 데이터 사전, 품질 전후, 세 가지 이상 변수 분석, 두 집계표, 두 그래프, 개선 제안, 분리된 테스트 평가, 기준선, 한계·편향·추가 데이터·개인정보 체크를 연결합니다. 모델은 실제 서비스 배포 대상이 아닙니다.

시작 전 상태와 완료 산출물​

확인 항목시작 조건완료 증거
입력 생성18강의 generate_model_data.py 존재고정 seed의 원본 241행
환경.venv에 pandas·matplotlib·scikit-learn 설치import 오류 없이 버전 기록
원본 보호data/raw는 읽기 전용 취급정제 결과가 원본을 덮어쓰지 않음
품질결측·중복·범위 위반 규칙 확정raw 241 → clean 238 근거
분석집계 기준과 단위 정의캐릭터·모드 CSV 2개
시각화축·범위·제목 정의PNG 2개
평가train/test 분리와 기준선train 178, test 60, JSON 지표
보고서주장과 한계 연결final_report.md 재생성 가능

기존 reports 파일이 있다면 실행 전에 생성 시각과 Git 상태를 확인합니다. 자동 생성 산출물을 사람이 쓴 원본처럼 직접 수정하지 말고, 변경이 필요하면 스크립트와 입력을 고친 뒤 다시 생성합니다.

구현 순서​

고정 가상 데이터 생성
→ 원본 품질 수치 기록
→ 중복·결측·범위 규칙 적용
→ 집계표와 그래프 생성
→ X/y 정의
→ train/test 분리
→ train에서 전처리·모델 학습
→ test에서 기준선과 모델 평가
→ 근거·제안·한계를 보고서로 연결

그래프나 모델부터 만들지 않습니다. 어떤 행을 제외했는지 기록하지 않으면 같은 점수와 그림을 다시 만들 수 없고, 제외 규칙이 결과를 유리하게 바꿨는지도 검토할 수 없습니다.

4. 단계별 실습​

1. 예제 파일 작성​

실행 환경: 코드 편집기

실행 위치: C:\dev\game-data-lab

실행 전 확인: 기존 파일이 있으면 내용을 덮어쓰지 말고 Git diff로 이번 강의 변경 범위를 확인합니다. 실제 개인정보·API Key·토큰은 사용하지 않습니다.

대상 파일: src/final_report.py

src/final_report.py
from pathlib import Path
import json
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder

SOURCE = Path("data/raw/game_sessions_model.csv")
TABLES, FIGURES = Path("reports/tables"), Path("reports/figures")
TABLES.mkdir(parents=True, exist_ok=True); FIGURES.mkdir(parents=True, exist_ok=True)
df = pd.read_csv(SOURCE, parse_dates=["match_date"])
quality = {
"rows": len(df),
"missing": df.isna().sum().astype(int).to_dict(),
"duplicate_sessions": int(df.duplicated("session_id").sum()),
"play_minutes_outside_1_180": int((~df["play_minutes"].between(1, 180)).sum()),
}
df = df.drop_duplicates("session_id").dropna(subset=["game_mode", "play_minutes", "score", "win", "character"])
df = df[df["play_minutes"].between(1, 180)].copy()
quality["clean_rows"] = len(df)

character = df.groupby("character").agg(matches=("win", "size"), win_rate=("win", "mean"), avg_score=("score", "mean")).reset_index()
mode = df.groupby("game_mode").agg(matches=("win", "size"), median_minutes=("play_minutes", "median"), avg_score=("score", "mean")).reset_index()
character.to_csv(TABLES / "character_summary.csv", index=False)
mode.to_csv(TABLES / "mode_summary.csv", index=False)

fig, ax = plt.subplots(); ax.bar(character["character"], character["win_rate"]); ax.set(ylim=(0, 1), title="Win rate by character", ylabel="win rate"); fig.tight_layout(); fig.savefig(FIGURES / "character_win_rate.png", dpi=150); plt.close(fig)
fig, ax = plt.subplots(); ax.scatter(df["play_minutes"], df["score"], alpha=.6); ax.set(title="Play time and score", xlabel="minutes", ylabel="score"); fig.tight_layout(); fig.savefig(FIGURES / "playtime_score.png", dpi=150); plt.close(fig)

X = df[["game_mode", "character"]]; y = df["win"].astype(int)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.25, random_state=42, stratify=y)
prep = ColumnTransformer([("cat", OneHotEncoder(handle_unknown="ignore"), ["game_mode", "character"])])
model = Pipeline([("prep", prep), ("model", LogisticRegression(max_iter=1000))]).fit(X_train, y_train)
baseline = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
def metrics(pred): return {"accuracy": round(accuracy_score(y_test, pred), 3), "precision": round(precision_score(y_test, pred, zero_division=0), 3), "recall": round(recall_score(y_test, pred, zero_division=0), 3)}
evaluation = {
"baseline": metrics(baseline.predict(X_test)),
"logistic_regression": metrics(model.predict(X_test)),
"train_rows": len(X_train),
"test_rows": len(X_test),
}
Path("reports/final_metrics.json").write_text(json.dumps({"quality": quality, "evaluation": evaluation}, ensure_ascii=False, indent=2), encoding="utf-8")

report = f"""# 게임 플레이 데이터 분석·예측 보고서

## 데이터 사전
한 행은 가상 플레이어의 완료 매치 한 건이다.

| 열 | 의미 | 타입·범위 |
|---|---|---|
| session_id | 가상 매치 결과 식별자 | 중복 없는 문자열 |
| player_id | 가상 플레이어 식별자 | 개인을 식별하지 않는 문자열 |
| game_mode | 게임 모드 | casual 또는 ranked |
| play_minutes | 완료 매치 플레이 시간 | 1~180 사이 정수 |
| score | 완료 매치 점수 | 0 이상 정수 |
| win | 승패 레이블 | 패배 0, 승리 1 |
| character | 사용 캐릭터 | Mage, Knight, Rogue |
| match_date | 매치 날짜 | YYYY-MM-DD |

## 품질과 분석
원본 {quality['rows']}행에서 결측·session_id 중복·1~180분 범위를 검사해 {quality['clean_rows']}행을 분석에 사용했다. 캐릭터 표와 모드 표, 승률 막대와 플레이 시간-점수 산점도를 생성했다. 평균만으로 결론 내리지 않고 경기 수와 중앙값을 함께 본다.

## 개선 제안
캐릭터별 승률 차이가 모드와 표본 구성에서도 유지되는지 추가 가상/내부 테스트한 뒤 작은 밸런스 실험을 한다.

## 예측과 평가
매치 시작 시점의 승패를 레이블로 정의하고, 이때 알 수 있는 모드와 캐릭터만 특성으로 사용한 뒤 train/test를 먼저 분리했다. 최종 점수와 플레이 시간은 누수 방지를 위해 특성에서 제외했다. 테스트 평가는 {evaluation}이다. 정확도뿐 아니라 정밀도와 재현율을 기준선과 비교한다.

## 한계·편향·추가 데이터
합성 규칙이 만든 패턴이므로 실제 플레이를 대표하지 않는다. 숙련도, 패치 버전, 팀 구성과 연결 품질이 추가로 필요하며 모드·캐릭터별 오류를 점검해야 한다.

## 개인정보 체크리스트
- 실제 이름·이메일·IP·토큰을 수집하지 않는다.
- 목적에 필요한 최소 열만 사용하고 보관 기간과 삭제 절차를 정한다.
- 가명 ID의 재식별·결합 위험과 접근 권한을 검토한다.
- 사용자 동의와 적용 법규는 실제 수집 전에 담당자와 확인한다.
"""
Path("reports/final_report.md").write_text(report, encoding="utf-8")
print(json.dumps(evaluation, ensure_ascii=False, indent=2))

코드의 핵심 단계와 값 흐름​

  1. random.Random(42)를 쓰는 18강 생성기가 원본을 결정하므로 같은 코드·환경에서 가상 행이 재현됩니다.
  2. quality는 정제 전 241행에서 score 결측 1, session_id 중복 1, play_minutes 범위 위반 1을 기록합니다.
  3. drop_duplicates가 마지막 중복 1행, dropna가 결측 1행, 범위 필터가 360분 1행을 제외해 238행을 남깁니다.
  4. groupby는 캐릭터와 모드별 경기 수·승률·점수 중심을 집계합니다. 경기 수가 다르므로 승률 숫자만 비교하지 않습니다.
  5. 두 그래프는 같은 정제 DataFrame에서 만들어져 표와 그림의 모집단이 일치합니다.
  6. X에는 매치 시작 시 알 수 있는 game_mode와 character만 두고, 종료 후 생기는 score·play_minutes는 승패 예측 특성에서 제외합니다.
  7. train_test_split(..., stratify=y)는 승패 비율을 비슷하게 유지하며 178행 학습·60행 테스트로 나눕니다.
  8. Pipeline 안의 OneHotEncoder가 학습 데이터에서 범주를 학습하고 같은 변환으로 테스트를 처리합니다.
  9. DummyClassifier는 가장 많은 클래스를 항상 예측하는 최소 기준입니다. 로지스틱 회귀가 이를 넘지 못하면 복잡한 모델을 쓸 근거가 없습니다.

손으로 확인할 품질과 집계 값​

18강의 고정 생성 코드를 그대로 사용하면 핵심 값은 다음과 같습니다.

품질 흐름
원본 행: 241
session_id 중복: 1
score 결측: 1
play_minutes 1~180 범위 밖: 1
정제 후 행: 241 - 1 - 1 - 1 = 238
승리: 139, 패배: 99
학습 행: 178, 테스트 행: 60
character_summary.csv를 반올림해 검산
Mage matches 91, win_rate 0.626, avg_score 1295.495
Knight matches 65, win_rate 0.538, avg_score 1318.092
Rogue matches 82, win_rate 0.573, avg_score 1289.183
합계 238경기
mode_summary.csv를 반올림해 검산
casual matches 107, median_minutes 27, avg_score 1232.084
ranked matches 131, median_minutes 25, avg_score 1354.550
합계 238경기

ranked 평균 점수가 더 높지만 생성 규칙에 ranked +80 보정이 포함됐고 skill·플레이 시간 구성도 영향을 줍니다. 이 표만으로 실제 ranked 모드가 실력을 높인다는 인과 결론을 낼 수 없습니다.

테스트 60행이 승리 35·패배 25로 나뉜다면 가장 빈번한 승리만 예측하는 기준선은 accuracy와 precision 약 0.583, recall 1.0입니다. 높은 recall은 모든 행을 승리라고 답한 결과일 수 있으므로 단독으로 좋은 모델을 뜻하지 않습니다. 로지스틱 회귀 지표는 생성된 JSON에서 확인하고 기준선과 세 지표를 함께 비교합니다.

2. 실행과 결과 확인​

실행 환경: Windows PowerShell

실행 위치: C:\dev\game-data-lab

실행 전 확인: Python 코드는 프로젝트의 .venv 인터프리터를 사용하고 입력 파일 경로가 존재하는지 확인합니다.

대상: src/final_report.py과 이 강의가 만드는 출력

Windows PowerShell
Set-Location C:\dev\game-data-lab
.\.venv\Scripts\python.exe .\src\generate_model_data.py
.\.venv\Scripts\python.exe .\src\final_report.py
Get-ChildItem .\reports\tables,.\reports\figures
Get-Content .\reports\final_report.md

예상 결과: 집계 CSV 2개, PNG 그래프 2개, 품질·평가 JSON과 분석·제안·한계·개인정보 체크가 담긴 최종 보고서가 생성됩니다.

reports 산출물
reports/
├─ tables/
│ ├─ character_summary.csv
│ └─ mode_summary.csv
├─ figures/
│ ├─ character_win_rate.png
│ └─ playtime_score.png
├─ final_metrics.json
└─ final_report.md

단순 파일 존재만 확인하지 않습니다. 두 CSV의 matches 합이 각각 238인지, PNG가 0바이트가 아니며 축 제목이 있는지, JSON의 train/test 행 합이 238인지, 보고서가 quality와 evaluation 값을 실제로 포함하는지 확인합니다.

5. 분석 또는 모델이 동작하는 이유​

하나의 스크립트가 고정 입력에서 품질 검사, 집계, 시각화, 분리, 전처리, 기준선·모델 평가와 보고서 생성을 반복합니다. Pipeline은 테스트 정보 누수를 줄이며 출력 파일은 주장과 근거를 함께 검토하게 합니다.

6. 자주 하는 실수와 해결법​

7. 직접 실습​

8. 이해 점검 질문 3개​

9. 핵심 요약​

다음 학습 연결​

분석 근거를 재현 가능한 결과물로 만들었습니다. 문제·선택·검증·한계를 포트폴리오 문서로 정리하려면 실제 문서인 실습·포트폴리오 1강으로 이어갑니다.

MINI QUIZ

최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성” 구현을 설계할 때 책임과 구조를 올바르게 나눈 선택은 무엇인가요?
  2. 문제 2‘그래프 파일만 제출하기’ 실수를 판단할 때 “최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성” 강의가 제시한 기준은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

22강. 최종 실습: 게임 플레이 데이터 분석과 간단한 예측 모델 보고서 완성 미완료 상태