0️⃣ 한글 폰트 설치¶
그래프에 한글이 깨지지 않도록 맨 먼저 아래 셀을 실행하세요. (Colab은 세션을 새로 켜면 초기화되니, 그때마다 다시 실행하면 됩니다.)
# 맨 처음 한 번만 실행 — 라이브러리 설치 + 한글폰트 { display-mode: "form" }
# 그래프에서 한글이 네모(□)로 깨지지 않게 폰트를 설정.
# Colab은 새로 켤 때마다 초기화되므로, 세션을 새로 시작하면 다시 실행.
!pip install -q ksme_visuals # 경기장 + 한글폰트 헬퍼가 든 라이브러리
from ksme_visuals import set_korean_font
set_korean_font(font="NanumGothic")
✅ 한글 폰트 설정 완료: NanumGothic
'NanumGothic'
1️⃣ 왜 파이썬으로 분석할까?¶
경기 하나에도 패스·슛·태클이 수천 건 쌓임. 손으로 세긴 불가능 파이썬은 이 수천 줄을 1초 만에 세고, 거르고, 그림으로 바꿔줌.
프로그래밍의 기본은 단순: 값에 이름을 붙이고(변수) → 그 값을 다루기.
print — 컴퓨터에게 말 시키기¶
print("안녕하세요, 경기분석가!")
print("서울", 4, "-", 2, "강원") # 쉼표로 여러 개를 한 줄에
안녕하세요, 경기분석가! 서울 4 - 2 강원
변수 — 값에 이름표 붙이기¶
등호(=)는 '같다'가 아니라 **'이름을 붙여 저장하라'**는 뜻
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()으로 종류를 확인할 수 있음
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 — 점유율 차이 출력¶
서울의 점유율 차이 출력해보기
# 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️⃣ 데이터를 담는 그릇 — 리스트와 딕셔너리¶
값 하나가 아니라 여러 개를 담아야 할 때가 많음 (선수 명단, 경기별 득점 등).
리스트 [ ] — 순서가 있는 값 묶음¶
# 서울 선수 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 — 묶음을 하나씩 꺼내 처리¶
for r in ratings:
judge = "👍 좋음" if r >= 7.5 else "보통"
print("평점", r, "→", judge)
평점 6.8 → 보통 평점 7.6 → 👍 좋음 평점 7.4 → 보통 평점 8.1 → 👍 좋음 평점 7.0 → 보통
✏️ 연습 2 — 평균 평점¶
리스트의 평균 평점을 구해 출력하세요. (힌트: 합 ÷ 개수)
# 1) avg = sum(ratings) / len(ratings)
# 2) round(avg, 2) 로 출력
avg = sum(ratings) / len(ratings)
print("평균 평점:", round(avg, 2))
평균 평점: 7.38
딕셔너리 { } — '이름표:값' 쌍¶
순서 대신 이름(key)으로 값을 꺼냅니다. 등번호→이름처럼 짝지을 때 최고.
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 선택.
import pandas as pd
xls="/content/match_events.xlsx"
시트(sheet) 골라 읽기¶
sheet_name= 으로 원하는 장을 지정합니다.
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)
| 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대 기본기¶
새 데이터를 받으면 항상 이 네 가지부터 봅니다.
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() — 숫자 열의 요약 통계(평균·최소·최대 등)를 한 번에.
events[["x", "y", "pass_length", "xg"]].describe().round(1)
| 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 |
열 하나 / 여러 개 꺼내기¶
events["player"].head() # 한 열
# 여러 열은 리스트로 감싸기:
events[["minute", "player", "event", "outcome"]].head()
| minute | player | event | outcome | |
|---|---|---|---|---|
| 0 | 0 | 이상헌 | 패스 | 성공 |
| 1 | 0 | 박청효 | 패스 | 실패 |
| 2 | 0 | 최준 | 클리어링 | 성공 |
| 3 | 0 | 홍철 | 패스 | - |
| 4 | 0 | 홍철 | 패스 | 성공 |
값 세기 — value_counts()¶
events["event"].value_counts()
| count | |
|---|---|
| event | |
| 패스 | 1005 |
| 볼리커버리 | 177 |
| 태클 | 70 |
| 클리어링 | 65 |
| 경합 | 54 |
| 블록된슈팅 | 49 |
| 차단 | 45 |
| 파울 | 37 |
| 슈팅 | 31 |
| 인터셉트 | 26 |
| 골키퍼 | 14 |
| 볼미스 | 10 |
| 드리블 | 10 |
| 실점 | 6 |
| 어시스트관련 | 4 |
| 수비액션 | 4 |
| 경고 | 2 |
✏️ 연습 3 — 팀별 이벤트 수¶
team 열로 팀별 이벤트 수를 세보기.
# 1) events["team"] 선택
# 2) .value_counts() 로 팀별 개수
events["team"].value_counts()
| count | |
|---|---|
| team | |
| 서울 | 855 |
| 강원 | 754 |
5️⃣ 원하는 행만 고르기 — 필터링¶
"서울의 슛만", "손흥민의 패스만" 처럼 조건에 맞는 행만 추리는 게 분석의 핵심.
조건식을 대괄호 [ ] 안에 넣기
# event가 '슈팅'인 행만
shots = events[events["event"] == "슈팅"]
print("슛 이벤트:", len(shots), "개")
shots[["minute", "team", "player", "x", "y", "outcome", "xg"]].head(10)
슛 이벤트: 31 개
| 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 |
조건 두 개 — &(그리고), |(또는)¶
각 조건을 괄호로 감싸기
# 서울의 슛만
seoul_shots = events[(events["team"] == "서울") & (events["event"] == "슈팅")]
print("서울 슛:", len(seoul_shots), "개")
seoul_shots[["minute", "player", "outcome", "xg"]]
서울 슛: 20 개
| 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 — 득점 장면 찾기¶
# 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
| 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("묶을기준")["볼열"].집계함수()
# 팀별 이벤트 종류 분포
events.groupby("team")["event"].value_counts().head(12)
| count | ||
|---|---|---|
| team | event | |
| 강원 | 패스 | 437 |
| 볼리커버리 | 92 | |
| 태클 | 42 | |
| 클리어링 | 33 | |
| 블록된슈팅 | 30 | |
| 경합 | 29 | |
| 차단 | 28 | |
| 파울 | 18 | |
| 인터셉트 | 13 | |
| 슈팅 | 11 | |
| 골키퍼 | 6 | |
| 드리블 | 5 |
선수별 패스 수 (많은 순)¶
passes = events[events["event"] == "패스"]
by_player = passes.groupby("player").size().sort_values(ascending=False)
by_player.head(10)
| 0 | |
|---|---|
| player | |
| 최준 | 81 |
| 김진수 | 65 |
| 서민우 | 55 |
| 강준혁 | 53 |
| 박성훈 | 53 |
| 야잔 | 50 |
| 안데르손 | 48 |
| 박수일 | 47 |
| 홍철 | 40 |
| 박청효 | 39 |
✏️ 연습 5 — 선수별 슈팅 수¶
선수별 슈팅 수를 많은 순으로 구하세요.
# 1) event=="슈팅" 행만 → shots
# 2) groupby("player").size()
# 3) sort_values(ascending=False) 로 내림차순
shots = events[events["event"] == "슈팅"]
shots.groupby("player").size().sort_values(ascending=False)
| 0 | |
|---|---|
| player | |
| 이승모 | 3 |
| 조영욱 | 3 |
| 린가드 | 3 |
| 야잔 | 2 |
| 김건희 | 2 |
| 구본철 | 2 |
| 둑스 | 2 |
| 문선민 | 2 |
| 안데르손 | 2 |
| 강준혁 | 1 |
| 김대원 | 1 |
| 모재현 | 1 |
| 류재문 | 1 |
| 김도현 | 1 |
| 김진수 | 1 |
| 이상헌 | 1 |
| 윤일록 | 1 |
| 천성훈 | 1 |
| 홍철 | 1 |
집계 함수 모음 — agg¶
개수만이 아니라 합/평균도 한 번에. 예: 선수별 패스 거리 평균과 최대.
passes.groupby("player")["pass_length"].agg(["count", "mean", "max"]).round(1).head(8)
| 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 방향으로 얼마나 전진했나)¶
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()
| 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로 한 번에!
# 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)
| is_success | |
|---|---|
| player | |
| 조영욱 | 100.0 |
| 이유현 | 90.0 |
| 김대우 | 90.0 |
| 이승모 | 88.9 |
| 서민우 | 87.3 |
| 류재문 | 86.7 |
| 이상헌 | 86.2 |
| 박성훈 | 84.9 |
| 안데르손 | 83.3 |
| 김건희 | 83.3 |
8️⃣ 표를 그림으로 — 첫 시각화¶
막대그래프 — 선수별 슈팅 수¶
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()
✏️ 연습 7 — 슛 위치 산점도 (xG 크기로)¶
슛이 어디서 나왔는지 점으로 찍어보세요.
# 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()