실습 - 축구 선수 데이터셋 살펴보기

Ryan·2025년 1월 24일

Python/Pandas

목록 보기
6/23

1. 데이터 불러오기 및 기본 정보 확인

python
복사
import pandas as pd
import matplotlib.pyplot as plt

players = pd.read_csv('players.csv')
players_valuations = pd.read_csv('player_valuations.csv')
  • players.csv : 선수 정보(이름, 생년월일, 출생 국가/도시, 포지션, 소속 클럽 등)를 담은 파일
  • player_valuations.csv : 선수 가치(연도별 시장 가치)를 담은 파일

두 CSV 파일을 불러와 각각 players, players_valuations DataFrame으로 저장했습니다.

python
복사
players.info()
players.columns
  • players.info() : 데이터프레임의 행(row), 열(column) 개수 및 각 컬럼의 결측치(null) 존재 여부, 데이터 타입 등을 확인합니다.
  • players.columns : 열 이름을 확인합니다.

컬럼 선택 및 결측치 처리

python
복사
cols = [
    'player_id', 'first_name', 'last_name', 'name', 'last_season',
    'current_club_id', 'player_code', 'country_of_birth', 'city_of_birth',
    'country_of_citizenship', 'date_of_birth', 'sub_position', 'position'
]
players = players[cols]
players.isnull().sum()  # 각 컬럼별 결측치 개수 확인
players.dropna(inplace=True)  # 결측치 제거
players.isnull().sum()
  • 불필요하거나 분석에 사용하지 않을 컬럼을 제외한 뒤, 결측치가 있는 행은 모두 제거합니다.
  • 프로젝트 목적에 따라 결측치를 제거(또는 대체)하는 방식을 결정합니다.

2. 탐색적 분석(Descriptive Statistics)

2.1 고유값(Unique Values) 개수 파악

python
복사
def count_value(df, column):
    count = len(df[column].unique())
    print(f'Total {column}: {count}')

ads = ['player_id', 'current_club_id', 'country_of_citizenship']

for column in ads:
    count_value(players, column)
  • player_id, current_club_id, country_of_citizenship 컬럼 각각에 대해 고유값의 개수를 확인합니다.
  • 예: country_of_citizenship에 대한 값들을 보면, 선수들이 어느 나라 출신이 가장 많은지, 대략적인 분포 파악이 가능합니다.

2.2 선수 가치 데이터 확인

python
복사
players_valuations.info()
players_valuations.describe()
  • players_valuations.info()를 통해 행/열 개수, 결측치 여부, 데이터 타입 확인
  • players_valuations.describe()로 기초 통계값(평균, 표준편차, 최소/최대, 사분위수 등) 확인
python
복사
mean_ = players_valuations['market_value_in_eur'].mean()
over_mean = len(players_valuations[players_valuations['market_value_in_eur'] > mean_])
total = len(players_valuations)
print(f'percentile of player over mean: {over_mean / total * 100: .2f}%')
  • 시장 가치(market_value_in_eur) 평균을 기준으로, 평균을 초과하는 선수 비율이 얼마나 되는지 계산했습니다.

3. 데이터 병합 및 전처리

3.1 playersplayers_valuations 병합(Merge)

python
복사
players_with_val = pd.merge(players, players_valuations, on='player_id')
  • player_id를 기준으로 두 DataFrame을 Inner Join(기본 설정)하여 하나의 DataFrame(players_with_val)으로 통합
python
복사
players_with_val['dateyear'] = players_with_val['date'].apply(lambda x: int(x[:4]))
players_with_val['age'] = players_with_val['dateyear'] - players_with_val['date_of_birth'].apply(lambda x: int(x[:4]))

players_with_val.drop_duplicates(['player_id', 'dateyear'], keep='last', inplace=True)
  • date 컬럼에서 연도(year)만 추출하여 dateyear라는 새로운 컬럼 생성
  • 출생 연도(date_of_birth)와 비교하여 선수 나이(age) 계산
  • (player_id, dateyear) 중복 행은 마지막 행만 남기고 제거
    • 예: 한 선수가 같은 연도의 여러 데이터(시가치 변동 등)가 있을 경우 마지막 업데이트 값을 사용

3.2 컬럼 정리 및 이름 변경

python
복사
cols_2 = [
    'player_id', 'current_club_id_y', 'first_name', 'last_name', 'name', 'last_season_x',
    'country_of_citizenship', 'city_of_birth', 'position', 'sub_position',
    'dateyear', 'age', 'market_value_in_eur'
]
players_with_val = players_with_val[cols_2]

players_with_val.rename(columns={
    'current_club_id_y':'current_club_id',
    'last_season_x':'last_season'
}, inplace=True)
  • 분석에 필요한 컬럼만 골라 최종 정리
  • 구분하기 어려운 컬럼(_y, _x) 등을 적절한 이름으로 변경

4. 데이터 분석 예시

4.1 2022 시즌 가치(Ranking)

python
복사
players_with_val_2022 = players_with_val[
    (players_with_val['dateyear'] == 2022) & (players_with_val['last_season'] == 2022)
]
players_with_val_2022['market_value_in_eur'] = players_with_val_2022['market_value_in_eur'].rank(method='min', ascending=False)
players_with_val_2022.sort_values(by='market_value_in_eur')
  • 2022년 데이터를 필터링하여, 시장 가치를 역순(높은 가치부터)으로 순위를 매깁니다.
  • sort_values(by='market_value_in_eur')로 정렬 후, 상위 선수가 누구인지 살펴봅니다.

5. 선수 가치 추이(Trend) 시각화

5.1 전체 선수 가치 분포(박스플롯, Boxplot)

python
복사
plt.figure(figsize=(8, 6))
plt.boxplot(players_with_val['market_value_in_eur'])
plt.show()
  • 전체 선수들의 시장 가치 분포를 박스플롯으로 확인
  • 이상치(outlier)를 확인하거나, 분포가 한쪽으로 치우쳐 있는지 확인하는 데 유리

5.2 시장 가치별 선수 분포(Count Plot)

python
복사
plt.figure(figsize=(8, 6))
players_with_val.groupby('market_value_in_eur')['player_id'].count().plot()
plt.show()
  • 가치가 특정 범위에 몰려있는지 확인할 수 있습니다.
  • 일부 고가치 선수가 소수에 불과하고, 대부분 중·저가치 구간에 집중되어 있을 가능성이 있습니다.

5.3 연도별 시장 가치 총합 및 최대값

python
복사
sum_per_year = players_with_val.groupby('dateyear')['market_value_in_eur'].sum()
max_per_year = players_with_val.groupby('dateyear')['market_value_in_eur'].max()
  • 연도별 총합 : 특정 연도에 등록된 선수들의 시장 가치를 모두 합산
  • 연도별 최대값 : 특정 연도에 가장 가치가 높은 선수가 어느 정도인지 파악
python
복사
plt.figure(figsize=(8, 6))
plt.plot(sum_per_year.index, sum_per_year.values)
plt.xticks(rotation=45)
plt.show()

plt.figure(figsize=(8, 6))
plt.plot(max_per_year.index, max_per_year.values)
plt.xticks(rotation=45)
plt.show()
  • 간단히 plt.plot으로 연도별 추세를 확인
  • 선수 가치가 특정 연도에 급등·급락하는지 또는 지속적으로 증가하는지 살펴볼 수 있습니다.

5.4 연도별 박스플롯

python
복사
players_with_val = players_with_val[(players_with_val['dateyear'] >= 2013) & (players_with_val['dateyear'] < 2023)]
years = sorted(players_with_val['dateyear'].unique())

num_plots = len(years)
num_rows = 4
num_cols = (num_plots + 3) // 4

fig, axes = plt.subplots(num_rows, num_cols, figsize=(12, 8))
axes = axes.flatten()

for i, year in enumerate(years):
    market_values = players_with_val[players_with_val['dateyear'] == year]['market_value_in_eur'].values
    ax = axes[i]
    ax.boxplot(market_values)
    ax.set_title(year)

for j in range(num_plots, num_rows * num_cols):
    fig.delaxes(axes[j])

plt.tight_layout()
plt.show()
  • 각 연도별로 시장 가치 분포가 어떻게 달라지는지 박스플롯으로 시각화
  • 연도별로 비교 시, 선수 가치가 높아진 추세인지, 분포가 넓어졌는지(고가치 선수 증가) 등을 파악

6. 변수 간 상관관계 분석

6.1 나이(age)와 시장 가치(market_value_in_eur)

python
복사
age_marget_values = players_with_val.groupby('age')['market_value_in_eur'].mean()
age_marget_values.plot(kind='bar')
  • 나이별 평균 시장 가치를 계산하여 막대그래프로 시각화
  • 전성기(가치가 높은 구간)가 몇 살 전후인지를 간접적으로 살펴볼 수 있음
python
복사
filtered_df = players_with_val[players_with_val['age'] <= 35]
age_marget_values = filtered_df.groupby('age')['market_value_in_eur'].mean()
sorted_values = age_marget_values.sort_values(ascending=False)
top_5_intervals = sorted_values.head(5).index
  • 과도하게 높은 나이(예: 40세 이상)를 제외하고(표본이 너무 적거나 특수 사례) 시각화
  • 나이별 평균 가치를 정렬하여, 가장 가치가 높은 Top 5 나이대 확인

막대 위에 선수 이름 표시하기

python
복사
top_players = filtered_df.groupby('age').apply(lambda x: x.loc[x['market_value_in_eur'].idxmax()]['name'])
age_marget_values.plot(kind='bar', color=colors)

for i, value in enumerate(age_marget_values):
    age = age_marget_values.index[i]
    top_player = top_players[age]
    # 막대 위에 최고 가치 선수 이름 표시
    plt.text(i, value, top_player, ha='center', va='bottom', color='black')

plt.xticks(rotation=45)
plt.show()
  • 나이별 최고 가치 선수를 찾고, 해당 막대 위에 이름을 표시하여 가독성 향상

7. 포지션별 시장 가치 분석

python
복사
position_market_values = players_with_val.groupby('position')['market_value_in_eur'].mean()
position_market_values.plot(kind='bar')
  • 포지션(position) 기준으로 그룹화 → 평균 시장 가치 비교
  • 공격수(FW), 미드필더(MF), 수비수(DF), 골키퍼(GK) 등 어떤 포지션이 평균적으로 가치가 높은지 파악

7.1 연도별 포지션 별 추이

python
복사
position_market_values = players_with_val.groupby(['position', 'dateyear'])['market_value_in_eur'].mean().reset_index()

for position in position_market_values['position'].unique():
    position_data = position_market_values[position_market_values['position'] == position]
    plt.plot(position_data['dateyear'], position_data['market_value_in_eur'], label=position)
plt.legend()
plt.show()
  • 포지션별로 연도별 평균 시장 가치가 어떻게 변동하는지 확인
  • 최근 트렌드에 따라 특정 포지션(예: 공격형 미드필더, 중앙 수비수 등)의 가치가 폭발적으로 증가할 수도 있음

7.2 세부 포지션(sub_position)

아래 코드는 포지션 → 세부 포지션(sub_position)으로 나누어 연도별 추이를 분석하려는 예시입니다.

사용 시, 실제로는 sub_position에 대한 데이터가 존재해야 하며, 코드의 변수명 확인이 필요합니다.


8. 국가별 분석

python
복사
country_player_counts = players_with_val.drop_duplicates('player_id')['country_of_citizenship'].value_counts()
top_10_countries = country_player_counts.head(10)
top_10_countries.plot(kind='bar')
  • 국가별 선수가 몇 명인지(중복 없는 player_id 기준) 확인
  • 어떤 국가 출신 선수가 가장 많은지 파악 (유럽 국가, 남미, 아시아 등)

8.1 지도 시각화 (geopy, plotly)

python
복사
from geopy.geocoders import Nominatim
import time
geolocator = Nominatim(user_agent='my-app')

locations = []
errors = []

countries = country_player_counts.index
for country in countries:
    try:
        location = geolocator.geocode(country)
        time.sleep(0.3)
        latitude = location.latitude
        longitude = location.longitude
        locations.append((country, latitude, longitude))
    except Exception as e:
        errors.append(country)
        continue

locations_df = pd.DataFrame(locations)
locations_df.rename(columns={0: 'country', 1: 'latitude', 2: 'longitude'}, inplace=True)
locations_df['player_counts'] = locations_df['country'].apply(lambda x: country_player_counts[x])
  • Nominatim(geopy) 라이브러리를 통해 국가 이름으로 위도/경도를 검색
  • 지도 시각화에 필요한 좌표 정보를 얻을 수 있음
  • 오류가 발생하는 국가 혹은 예외가 있으면(errors 리스트) 처리
python
복사
import plotly.express as px

fig = px.density_mapbox(
    locations_df, lat='latitude', lon='longitude',
    z='player_counts', radius=15,
    center=dict(lat=0, lon=180), zoom=0,
    mapbox_style='stamen-terrian'
)
fig.show()
  • Plotly를 사용해 세계 지도 위에 선수 수 분포를 히트맵 형태로 표시
  • 직관적으로 어느 대륙/국가에 선수 분포가 집중되어 있는지 시각화

8.2 잉글랜드(England) 선수 출생 도시 분석

python
복사
cities_in_england = players_with_val[players_with_val['country_of_citizenship'] == 'England'].drop_duplicates('player_id')['city_of_birth']
england_players_count = cities_in_england.value_counts()
top_10_city_in_england = england_players_count.head(10)

plt.figure(figsize=(12,8))
top_10_city_in_england.plot(kind='bar')
for i, value in enumerate(top_10_city_in_england):
    plt.text(i, value, str(value), ha='center', va='bottom')
plt.show()
  • 잉글랜드 출생 선수들 중 출생 도시(city_of_birth)를 기준으로, 어느 도시 출신 선수가 가장 많은지 분석

정리 및 결론

  1. 데이터 정제 & 전처리
    • 불필요한 컬럼 제거, 결측치 제거 혹은 대체, 중복 처리 등을 통해 분석에 적합한 형태로 만들었습니다.
    • 특히 연도(dateyear), 나이(age) 등을 파생 변수로 생성해 시계열·분포 분석에 활용.
  2. 탐색적 분석(EDA)과 시각화
    • 박스플롯, 막대 그래프, 라인 플롯 등을 통해 선수들의 시장 가치 추이, 나이와 가치의 관계, 포지션별·국가별 분포 등을 분석하였습니다.
    • 특정 연도에 고가치 선수가 몰려있는지(분포 형태), 전성기 나이가 몇 살 전후인지 등을 간략히 살펴볼 수 있었습니다.
  3. 포지션·나이·국가 등 다양한 변수 연계
    • 포지션별로 연도에 따라 가치가 달라지는 추세를 파악하면, 리그 트렌드 변화(수비형 미드필더 중요성, 전방 압박 스타일 강조 등)와도 연관지어 볼 수 있습니다.
    • 나이에 따른 가치 변화를 보면, 전성기와 가치 급등 시기를 가늠할 수 있고, 국가별 분포를 통해 유망 선수 풀이 어디에 많은지도 가늠할 수 있습니다.
  4. 지도 시각화
    • geopy, plotly 등 외부 라이브러리를 활용하여, 선수들이 출생한 국가(도시) 분포를 지도로 표시할 수 있습니다.
    • 세계 각국에서 배출된 선수 규모를 히트맵으로 표현하면, 특정 지역(예: 남미, 유럽 등) 집중 현상을 한눈에 파악 가능.

추가 팁 및 확장 아이디어

  • 시간별 세분화 : 연도뿐 아니라 분기, 월 단위로 세분화하면 중간 이적 등에 대한 추이도 볼 수 있습니다.
  • 선수별 세부 추이 : 특정 스타 선수(예: 손흥민, 리오넬 메시, 크리스티아누 호날두 등)에 대해, 해마다 시장 가치가 어떻게 변동했는지 개별 선수 그래프를 그릴 수 있습니다.
  • 인기 포지션·세부 포지션 : 수비수가 공격수보다 가치가 낮다고 해석하기보다는, 전체 선수풀이 어떻게 구성되어 있는지, 이적 시장 수요 등이 반영되어 있음도 함께 고려해야 합니다.
  • 가치의 로그 변환 : 고가치 선수가 소수에 불과해 분포가 왜곡될 수 있으므로, 필요하다면 시장 가치에 로그 변환(np.log)을 적용해 데이터의 편향(skewness)을 줄여볼 수도 있습니다.

이상으로, 축구 선수 데이터를 활용한 기본적인 전처리, EDA, 시각화 과정을 정리해보았습니다. 데이터분석 과정에서 가장 중요한 것은 “무엇을 알고 싶은지”입니다.

0개의 댓글