스포츠 경기분석과 데이터사이언스(강지연)¶

스포츠경기분석가 아카데미(기초과정)

데이터 2025 K리그1 34R — FC서울 4 : 2 강원FC


수업 내용¶

파트 내용 키워드
1 왜 파이썬인가 + 첫 코드 변수, 출력
2 데이터를 담는 그릇 리스트, 딕셔너리
3 표 다루기의 시작 pandas, 엑셀 읽기
4 표 살펴보기 head/shape/describe
5 원하는 행만 고르기 필터링
6 묶어서 세고 요약 groupby
7 새 정보 만들기 파생변수, 정렬
8 표를 그림으로 막대/산점도

0️⃣ 한글 폰트 설치¶

그래프에 한글이 깨지지 않도록 맨 먼저 아래 셀을 실행하세요. (Colab은 세션을 새로 켜면 초기화되니, 그때마다 다시 실행하면 됩니다.)

In [ ]:
# 맨 처음 한 번만 실행 — 라이브러리 설치 + 한글폰트 { display-mode: "form" }
# 그래프에서 한글이 네모(□)로 깨지지 않게 폰트를 설정.
# Colab은 새로 켤 때마다 초기화되므로, 세션을 새로 시작하면 다시 실행.

!pip install -q ksme_visuals                 # 경기장 + 한글폰트 헬퍼가 든 라이브러리

from ksme_visuals import set_korean_font
set_korean_font(font="NanumGothic")
✅ 한글 폰트 설정 완료: NanumGothic
Out[ ]:
'NanumGothic'

1️⃣ 왜 파이썬으로 분석할까?¶

경기 하나에도 패스·슛·태클이 수천 건 쌓임. 손으로 세긴 불가능 파이썬은 이 수천 줄을 1초 만에 세고, 거르고, 그림으로 바꿔줌.

프로그래밍의 기본은 단순: 값에 이름을 붙이고(변수) → 그 값을 다루기.

print — 컴퓨터에게 말 시키기¶

In [ ]:
print("안녕하세요, 경기분석가!")
print("서울", 4, "-", 2, "강원")   # 쉼표로 여러 개를 한 줄에
안녕하세요, 경기분석가!
서울 4 - 2 강원

변수 — 값에 이름표 붙이기¶

등호(=)는 '같다'가 아니라 **'이름을 붙여 저장하라'**는 뜻

In [ ]:
home_team = "서울"
away_team = "강원"
home_goals = 4
away_goals = 2

print(home_team, "팀이", home_goals, "골 넣었습니다")
print("골 차이:", home_goals - away_goals)   # 숫자는 계산도 됨
서울 팀이 4 골 넣었습니다
골 차이: 2

자료형 — 값에도 '종류'가 있다¶

  • 숫자(정수 int, 소수 float) · 글자(문자열 str) · 참/거짓(bool)
  • type() 으로 종류를 확인할 수 있음
In [ ]:
print(type(home_goals))      # <class 'int'>  정수
print(type(7.5))             # <class 'float'> 소수
print(type(home_team))       # <class 'str'>   글자
print(type(home_goals > away_goals))   # <class 'bool'> 참/거짓
<class 'int'>
<class 'float'>
<class 'str'>
<class 'bool'>

✏️ 연습 1 — 점유율 차이 출력¶

서울의 점유율 차이 출력해보기

In [ ]:
# 1) seoul_possession=58, gangwon_possession=42 변수 지정
# 2) diff = 서울 - 강원
# 3) print 로 diff 출력

seoul_possession = 58
gangwon_possession = 42
diff = seoul_possession - gangwon_possession
print("점유율 차이:", diff, "%")
점유율 차이: 16 %

2️⃣ 데이터를 담는 그릇 — 리스트와 딕셔너리¶

값 하나가 아니라 여러 개를 담아야 할 때가 많음 (선수 명단, 경기별 득점 등).

리스트 [ ] — 순서가 있는 값 묶음¶

In [ ]:
# 서울 선수 5명의 이번 경기 평점
ratings = [6.8, 7.6, 7.4, 8.1, 7.0]

print("선수 수:", len(ratings))      # len = 개수
print("첫 선수 평점:", ratings[0])    # 0번부터 시작!
print("마지막 평점:", ratings[-1])    # -1 = 맨 뒤
print("평점 합:", sum(ratings))
print("최고 평점:", max(ratings))
선수 수: 5
첫 선수 평점: 6.8
마지막 평점: 7.0
평점 합: 36.9
최고 평점: 8.1

반복문 for — 묶음을 하나씩 꺼내 처리¶

In [ ]:
for r in ratings:
    judge = "👍 좋음" if r >= 7.5 else "보통"
    print("평점", r, "→", judge)
평점 6.8 → 보통
평점 7.6 → 👍 좋음
평점 7.4 → 보통
평점 8.1 → 👍 좋음
평점 7.0 → 보통

✏️ 연습 2 — 평균 평점¶

리스트의 평균 평점을 구해 출력하세요. (힌트: 합 ÷ 개수)

In [ ]:
# 1) avg = sum(ratings) / len(ratings)
# 2) round(avg, 2) 로 출력

avg = sum(ratings) / len(ratings)
print("평균 평점:", round(avg, 2))
평균 평점: 7.38

딕셔너리 { } — '이름표:값' 쌍¶

순서 대신 이름(key)으로 값을 꺼냅니다. 등번호→이름처럼 짝지을 때 최고.

In [ ]:
roster = {7: "린가드", 9: "천성훈", 16: "최준"}
print("9번은?", roster[9])
roster[10] = "류재문"          # 추가
print("전체:", roster)
print("등번호들:", list(roster.keys()))
print("이름들:", list(roster.values()))
9번은? 천성훈
전체: {7: '린가드', 9: '천성훈', 16: '최준', 10: '류재문'}
등번호들: [7, 9, 16, 10]
이름들: ['린가드', '천성훈', '최준', '류재문']

3️⃣ 표 다루기의 시작 — pandas¶

실제 경기 데이터는 엑셀 표 형태. 이런 표를 다루는 도구가 pandas. 표 한 장 전체를 DataFrame(데이터프레임)이라 부름.

오늘 엑셀(match_events.xlsx)에는 **여러 장(시트)**이 들어있음:

  • events — 경기 중 모든 행동(패스/슛/태클…) 한 줄씩 (1,600여 건!)
  • players — 선수별 기록 요약
  • team_stats — 팀 종합 스탯
  • pass_matrix — 선수 간 패스 횟수
  • 경기정보, 코드설명 — 참고용

엑셀 올리기¶

파일 선택창에서 match_events.xlsx 선택.

In [ ]:
import pandas as pd
xls="/content/match_events.xlsx"

시트(sheet) 골라 읽기¶

sheet_name= 으로 원하는 장을 지정합니다.

In [ ]:
events = pd.read_excel(xls, sheet_name="events")     # 이벤트 로그
players = pd.read_excel(xls, sheet_name="players")    # 선수 요약
team    = pd.read_excel(xls, sheet_name="team_stats") # 팀 스탯

print("events:", events.shape, "  players:", players.shape)
events.head()      # 위에서 5줄 미리보기
events: (1609, 18)   players: (40, 13)
Out[ ]:
event_id minute period team player back_no event outcome code x y end_x end_y pass_length key_pass assist xg xpass
0 1 0 1 강원 이상헌 22 패스 성공 PSS 52.5 33.5 22.6 27.4 30.5 N N NaN 0.317
1 2 0 1 강원 박청효 21 패스 실패 PSU 27.1 26.2 69.2 55.7 51.5 N N NaN 0.965
2 3 0 1 서울 최준 16 클리어링 성공 CLG 69.0 55.4 NaN NaN NaN N N NaN NaN
3 4 0 1 강원 홍철 33 패스 - PS 75.4 68.0 57.9 61.0 18.9 N N NaN 0.415
4 5 0 1 강원 홍철 33 패스 성공 PSS 75.4 68.0 57.9 61.0 18.9 N N NaN 0.425

4️⃣ 표 살펴보기 — 4대 기본기¶

새 데이터를 받으면 항상 이 네 가지부터 봅니다.

In [ ]:
print("크기 (행, 열):", events.shape)
print("열 이름:", list(events.columns))
print("자료형 / 결측")
events.info()
크기 (행, 열): (1609, 18)
열 이름: ['event_id', 'minute', 'period', 'team', 'player', 'back_no', 'event', 'outcome', 'code', 'x', 'y', 'end_x', 'end_y', 'pass_length', 'key_pass', 'assist', 'xg', 'xpass']
자료형 / 결측
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1609 entries, 0 to 1608
Data columns (total 18 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   event_id     1609 non-null   int64  
 1   minute       1609 non-null   int64  
 2   period       1609 non-null   int64  
 3   team         1609 non-null   object 
 4   player       1609 non-null   object 
 5   back_no      1609 non-null   int64  
 6   event        1609 non-null   object 
 7   outcome      1609 non-null   object 
 8   code         1609 non-null   object 
 9   x            1609 non-null   float64
 10  y            1609 non-null   float64
 11  end_x        1199 non-null   float64
 12  end_y        1185 non-null   float64
 13  pass_length  1168 non-null   float64
 14  key_pass     1609 non-null   object 
 15  assist       1609 non-null   object 
 16  xg           36 non-null     float64
 17  xpass        1055 non-null   float64
dtypes: float64(7), int64(4), object(7)
memory usage: 226.4+ KB

describe() — 숫자 열의 요약 통계(평균·최소·최대 등)를 한 번에.

In [ ]:
events[["x", "y", "pass_length", "xg"]].describe().round(1)
Out[ ]:
x y pass_length xg
count 1609.0 1609.0 1168.0 36.0
mean 57.5 33.2 17.9 0.1
std 26.6 21.2 13.1 0.2
min 0.0 0.0 0.8 0.0
25% 38.4 14.7 8.9 0.0
50% 58.9 33.0 14.1 0.1
75% 77.2 51.5 22.9 0.2
max 105.0 68.0 74.0 0.8

열 하나 / 여러 개 꺼내기¶

In [ ]:
events["player"].head()                 # 한 열
# 여러 열은 리스트로 감싸기:
events[["minute", "player", "event", "outcome"]].head()
Out[ ]:
minute player event outcome
0 0 이상헌 패스 성공
1 0 박청효 패스 실패
2 0 최준 클리어링 성공
3 0 홍철 패스 -
4 0 홍철 패스 성공

값 세기 — value_counts()¶

In [ ]:
events["event"].value_counts()
Out[ ]:
count
event
패스 1005
볼리커버리 177
태클 70
클리어링 65
경합 54
블록된슈팅 49
차단 45
파울 37
슈팅 31
인터셉트 26
골키퍼 14
볼미스 10
드리블 10
실점 6
어시스트관련 4
수비액션 4
경고 2

✏️ 연습 3 — 팀별 이벤트 수¶

team 열로 팀별 이벤트 수를 세보기.

In [ ]:
# 1) events["team"] 선택
# 2) .value_counts() 로 팀별 개수

events["team"].value_counts()
Out[ ]:
count
team
서울 855
강원 754

5️⃣ 원하는 행만 고르기 — 필터링¶

"서울의 슛만", "손흥민의 패스만" 처럼 조건에 맞는 행만 추리는 게 분석의 핵심. 조건식을 대괄호 [ ] 안에 넣기

In [ ]:
# event가 '슈팅'인 행만
shots = events[events["event"] == "슈팅"]
print("슛 이벤트:", len(shots), "개")
shots[["minute", "team", "player", "x", "y", "outcome", "xg"]].head(10)
슛 이벤트: 31 개
Out[ ]:
minute team player x y outcome xg
18 1 강원 홍철 81.5 40.4 막힘 0.019
69 4 서울 둑스 9.0 32.0 유효 0.068
130 9 강원 이상헌 83.3 22.0 유효 0.016
141 10 강원 김건희 86.6 18.0 빗나감 0.024
157 11 강원 김건희 100.9 39.6 골 0.307
321 21 서울 이승모 3.5 40.3 유효 0.195
421 25 서울 안데르손 17.0 30.4 막힘 0.067
465 29 강원 강준혁 90.7 40.5 막힘 0.097
522 35 서울 이승모 22.2 40.6 막힘 0.039
554 38 서울 김진수 25.8 29.2 빗나감 0.025

조건 두 개 — &(그리고), |(또는)¶

각 조건을 괄호로 감싸기

In [ ]:
# 서울의 슛만
seoul_shots = events[(events["team"] == "서울") & (events["event"] == "슈팅")]
print("서울 슛:", len(seoul_shots), "개")
seoul_shots[["minute", "player", "outcome", "xg"]]
서울 슛: 20 개
Out[ ]:
minute player outcome xg
69 4 둑스 유효 0.068
321 21 이승모 유효 0.195
421 25 안데르손 막힘 0.067
522 35 이승모 막힘 0.039
554 38 김진수 빗나감 0.025
763 50 이승모 유효 0.017
804 45 조영욱 빗나감 0.075
974 58 야잔 유효 0.104
1027 61 둑스 유효 0.223
1094 66 안데르손 막힘 0.051
1107 66 야잔 유효 0.038
1149 70 조영욱 빗나감 0.306
1173 72 린가드 골 0.263
1218 74 조영욱 유효 0.089
1252 77 린가드 골 0.047
1266 79 류재문 골 0.038
1331 85 린가드 빗나감 0.020
1377 89 문선민 빗나감 0.252
1508 97 문선민 빗나감 0.380
1541 99 천성훈 골 0.299

✏️ 연습 4 — 득점 장면 찾기¶

In [ ]:
# 1) events[events["team"]=="강원"] → gangwon, len 출력
# 2) events[events["outcome"]=="골"] → goals
# 3) goals 에서 minute/team/player/x/y 열 보기

gangwon = events[events["team"] == "강원"]
print("강원 이벤트:", len(gangwon))

goals = events[events["outcome"] == "골"]
goals[["minute", "team", "player", "x", "y"]]
강원 이벤트: 754
Out[ ]:
minute team player x y
157 11 강원 김건희 100.9 39.6
873 52 강원 모재현 11.0 33.9
1173 72 서울 린가드 97.9 33.6
1252 77 서울 린가드 84.4 57.3
1266 79 서울 류재문 84.4 27.7
1541 99 서울 천성훈 93.8 32.4

6️⃣ 묶어서 세고 요약 — groupby¶

"선수별 패스 수", "팀별 슛 수"처럼 같은 것끼리 묶어 집계합니다. 패턴: df.groupby("묶을기준")["볼열"].집계함수()

In [ ]:
# 팀별 이벤트 종류 분포
events.groupby("team")["event"].value_counts().head(12)
Out[ ]:
count
team event
강원 패스 437
볼리커버리 92
태클 42
클리어링 33
블록된슈팅 30
경합 29
차단 28
파울 18
인터셉트 13
슈팅 11
골키퍼 6
드리블 5

선수별 패스 수 (많은 순)¶

In [ ]:
passes = events[events["event"] == "패스"]
by_player = passes.groupby("player").size().sort_values(ascending=False)
by_player.head(10)
Out[ ]:
0
player
최준 81
김진수 65
서민우 55
강준혁 53
박성훈 53
야잔 50
안데르손 48
박수일 47
홍철 40
박청효 39

✏️ 연습 5 — 선수별 슈팅 수¶

선수별 슈팅 수를 많은 순으로 구하세요.

In [ ]:
# 1) event=="슈팅" 행만 → shots
# 2) groupby("player").size()
# 3) sort_values(ascending=False) 로 내림차순

shots = events[events["event"] == "슈팅"]
shots.groupby("player").size().sort_values(ascending=False)
Out[ ]:
0
player
이승모 3
조영욱 3
린가드 3
야잔 2
김건희 2
구본철 2
둑스 2
문선민 2
안데르손 2
강준혁 1
김대원 1
모재현 1
류재문 1
김도현 1
김진수 1
이상헌 1
윤일록 1
천성훈 1
홍철 1

집계 함수 모음 — agg¶

개수만이 아니라 합/평균도 한 번에. 예: 선수별 패스 거리 평균과 최대.

In [ ]:
passes.groupby("player")["pass_length"].agg(["count", "mean", "max"]).round(1).head(8)
Out[ ]:
count mean max
player
강준혁 53 14.0 37.8
강투지 33 15.7 43.0
강현무 34 35.5 62.7
구본철 14 12.0 34.1
김건희 12 11.3 40.5
김대우 10 14.0 33.7
김대원 38 18.8 57.3
김도현 6 14.6 22.0

7️⃣ 새 정보 만들기 — 파생변수 & 정렬¶

원본에 없던 값을 계산해서 새 열로 만들면 분석이 깊어집니다.

예: 패스의 전진 거리 (x 방향으로 얼마나 전진했나)¶

In [ ]:
p = events[events["event"] == "패스"].copy()    # copy로 안전하게
p["forward"] = p["end_x"] - p["x"]              # 새 열 = 끝 - 시작
# 가장 전진한 패스 top 5
p.sort_values("forward", ascending=False)[
    ["minute", "team", "player", "x", "end_x", "forward"]].head()
Out[ ]:
minute team player x end_x forward
735 49 강원 박청효 14.2 76.4 62.2
426 27 강원 박청효 16.5 78.3 61.8
1563 101 서울 강현무 5.1 64.8 59.7
1311 83 서울 강현무 4.8 62.9 58.1
1457 93 서울 최준 13.5 70.9 57.4

✏️ 연습 6 — 선수별 '패스 성공률' 구하기¶

패스 성공률 = 성공 패스 수 ÷ 전체 패스 수. groupby로 한 번에!

In [ ]:
# 1) event=="패스" 행만 copy → p
# 2) p["is_success"] = (outcome=="성공").astype(int)
# 3) groupby("player")["is_success"] → g
# 4) g.mean()[g.count()>=10] *100 (10개 이상)
# 5) round(1).sort_values 내림차순 후 상위 10 출력

p = events[events["event"] == "패스"].copy()
p["is_success"] = (p["outcome"] == "성공").astype(int)
g = p.groupby("player")["is_success"]
result = (g.mean()[g.count() >= 10] * 100).round(1).sort_values(ascending=False) #Boolean Filtering으로 패스 수 10개 이상인 선수만 남기기
result.head(10)
Out[ ]:
is_success
player
조영욱 100.0
이유현 90.0
김대우 90.0
이승모 88.9
서민우 87.3
류재문 86.7
이상헌 86.2
박성훈 84.9
안데르손 83.3
김건희 83.3

8️⃣ 표를 그림으로 — 첫 시각화¶

막대그래프 — 선수별 슈팅 수¶

In [ ]:
import matplotlib.pyplot as plt

shots = events[events["event"] == "슈팅"]
top = shots.groupby("player").size().sort_values(ascending=False).head(8) #size() : Nan 포함/ count() : Nan 포함 안함

plt.figure(figsize=(8, 4.5))
top.plot(kind="bar", color="#4361ee")
plt.title("선수별 슛 수 (TOP 8)")
plt.ylabel("슈팅 수"); plt.xticks(rotation=45)
plt.tight_layout(); plt.show()
No description has been provided for this image

✏️ 연습 7 — 슛 위치 산점도 (xG 크기로)¶

슛이 어디서 나왔는지 점으로 찍어보세요.

In [ ]:
# 1) 서울 & 슈팅 행만 → seoul_shots
# 2) plt.figure(figsize=(8,5))
# 3) 점 크기 sizes = xg.fillna(0.05)*800+40
# 4) outcome 별 색 colors = outcome.map({...}).fillna("gray")
# 5) plt.scatter(x, y, s=sizes, c=colors)
# 6) title/xlabel/ylabel, xlim(0,105) ylim(0,68), show

seoul_shots = events[(events["team"]=="서울") & (events["event"]=="슈팅")]

plt.figure(figsize=(8, 5))
# 점 크기 = 기대득점(xG)*800 → 좋은 찬스일수록 큰 점
sizes = seoul_shots["xg"].fillna(0.05) * 800 + 40
colors = seoul_shots["outcome"].map(
    {"골":"gold", "유효":"crimson", "빗나감":"gray", "막힘":"#888"}).fillna("gray")
plt.scatter(seoul_shots["x"], seoul_shots["y"], s=sizes, c=colors, edgecolors="black")
plt.title("서울 슛 위치 (점 크기=기대득점 xG)")
plt.xlabel("길이 x (m)"); plt.ylabel("폭 y (m)")
plt.xlim(0, 105); plt.ylim(0, 68)
plt.show()
No description has been provided for this image
In [ ]: