본문 바로가기

FootballAnalysis

[K리그 데이터 분석] 3. 데이터 전처리 및 변환

 

K리그 축구 전문 데이터 포털

K리그의 모든 것이 담겨있습니다. 경기전 관전포인트 부터 전문 분석 매치서머리까지 지금 방문해보세요. 로그인 없이 이용하실 수 있습니다.

data.kleague.com

 

이번 포스팅에선 이전 포스팅에서 수집한 데이터를 분석 가능한 형태로 전처리하는 코드를 다루려고 한다.

# 변환 적용을 위한 쉼표 제거를 위한 함수
def remove_commas(value):
    if isinstance(value, str):
        return value.replace(",", "")
    return value


# 변환 적용을 위한 쉼표 제거 및 정수 변환 함수
def convert_to_int(value):
    value = remove_commas(value)
    try:
        return int(value)
    except ValueError:
        return value

 # 드리블 성공% 및 패스 성공% 등 퍼센트 컬럼에 대해서는 따로 처리하지 않음
percent_columns = [
    '드리블 성공%', '패스 성공%', '전방 패스 성공%', '후방 패스 성공%', '횡패스 성공%',
    '공격지역패스 성공%', '수비지역패스 성공%', '중앙지역패스 성공%', '롱패스 성공%', '중거리패스 성공%',
    '숏패스 성공%', '크로스 성공%', '경합 지상 성공%', '경합 공중 성공%', '태클 성공%'
]

# HTML에서 추출된 문자열로 이루어진 데이터를 정수형으로 변환
for col in df.columns:
    if col not in percent_columns:
        df[col] = df[col].apply(convert_to_int)

 

수집한 데이터는 HTML에서 크롤링한 것이어서 모두 string이고 ',(쉼표)'가 추가되어있다.

정수형으로 바꿔서 이후 전처리를 위한 타입으로 변환하였다.

이때, 쉼표를 포함하지 않는(1000 이상의 수가 존재할 수 없는) 열은 제외 해주었다.

# 선수 이름과 포지션을 기준으로 그룹화하여 합계 계산
    # 합계를 구할 수 없는 열(예: 선수 이름, 포지션 등)은 첫 번째 값으로 대체
    grouped_df = df.groupby(['선수명', '구단'], as_index=False).agg(
        {
            '선수명': 'first',
            '구단': 'first',
            '포지션': 'first',
            '등번호': 'first',
            '출전시간(분)': 'sum',
            '득점': 'sum',
            '도움': 'sum',
            '슈팅': 'sum',
            '유효 슈팅': 'sum',
            '차단된슈팅': 'sum',
            '벗어난슈팅': 'sum',
            'PA내 슈팅': 'sum',
            'PA외 슈팅': 'sum',
            '오프사이드': 'sum',
            '프리킥': 'sum',
            '코너킥': 'sum',
            '스로인': 'sum',
            '드리블 시도': 'sum',
            '드리블 성공': 'sum',
            '드리블 성공%': 'mean',
            '패스 시도': 'sum',
            '패스 성공': 'sum',
            '패스 성공%': 'mean',
            '키패스': 'sum',
            '전방 패스 시도': 'sum',
            '전방 패스 성공': 'sum',
            '전방 패스 성공%': 'mean',
            '후방 패스 시도': 'sum',
            '후방 패스 성공': 'sum',
            '후방 패스 성공%': 'mean',
            '횡패스 시도': 'sum',
            '횡패스 성공': 'sum',
            '횡패스 성공%': 'mean',
            '공격지역패스 시도': 'sum',
            '공격지역패스 성공': 'sum',
            '공격지역패스 성공%': 'mean',
            '수비지역패스 시도': 'sum',
            '수비지역패스 성공': 'sum',
            '수비지역패스 성공%': 'mean',
            '중앙지역패스 시도': 'sum',
            '중앙지역패스 성공': 'sum',
            '중앙지역패스 성공%': 'mean',
            '롱패스 시도': 'sum',
            '롱패스 성공': 'sum',
            '롱패스 성공%': 'mean',
            '중거리패스 시도': 'sum',
            '중거리패스 성공': 'sum',
            '중거리패스 성공%': 'mean',
            '숏패스 시도': 'sum',
            '숏패스 성공': 'sum',
            '숏패스 성공%': 'mean',
            '크로스 시도': 'sum',
            '크로스 성공': 'sum',
            '크로스 성공%': 'mean',
            '경합 지상 시도': 'sum',
            '경합 지상 성공': 'sum',
            '경합 지상 성공%': 'mean',
            '경합 공중 시도': 'sum',
            '경합 공중 성공': 'sum',
            '경합 공중 성공%': 'mean',
            '태클 시도': 'sum',
            '태클 성공': 'sum',
            '태클 성공%': 'mean',
            '클리어링': 'sum',
            '인터셉트': 'sum',
            '차단': 'sum',
            '획득': 'sum',
            '블락': 'sum',
            '볼미스': 'sum',
            '파울': 'sum',
            '피파울': 'sum',
            '경고': 'sum',
            '퇴장': 'sum',
        }
    ).reset_index()

 

K리그 데이터 포털에 선수 데이터가 저장되어 있는 형식을 보면,

한 선수가 선발 출전하였을 때의 데이터와 교체로 투입되었을 때의 데이터가 다른 행에 저장되어있다.

각각 두 행을 병합하기 위해 다음과 같은 코드를 작성하였다.

이때, reset_index() 메서드를 사용하여 groupbyagg를 통해 생성된 결과의 인덱스를 초기화하고, 기존의 ['선수명', '포지션', '구단'] 열을 DataFrame의 일반 열로 변환하였다. 이를 통해 데이터를 병합하거나 다른 처리를 할 때 인덱스가 아닌 열을 기준으로 작업할 수 있었다.

# xG 데이터 기존 데이터 프레임에 병합
    xg_df = xg_df.drop(columns=['순위', '출전수', '출전시간(분)', '슈팅', '득점', '90분당 xG'])
    merge_condition = ['선수명', '구단']
    merged_df = pd.merge(grouped_df, xg_df, on=merge_condition, how='outer')
    merged_df = merged_df.drop_duplicates(subset='index', keep='first')

 

그 다음엔 병합된 데이터와 이전 포스팅에서 수집한 xG(기대득점) 데이터를 병합해주었다.
그리고 중복된 이전 과정에서 병합되지 않은 행을 index를 기준으로 다시 병합하였다.

# 대상 컬럼의 데이터를 (출전시간 / 90)으로 나누어 경기 당 이벤트 데이터로 변환
    for col in columns_to_normalize[:-1]:  # 'xG/득점' 열을 대상에서 제외
        num_matches = merged_df['출전시간(분)'] / 90
        num_matches.replace(0, np.nan, inplace=True)  # 0을 NaN으로 대체하여 무한대 값 발생 방지
        merged_df[col] = merged_df[col] / num_matches
    merged_df = merged_df.fillna(0)  # NaN 값을 0으로 대체

 

columns_to_normalize에 데이터를 출전시간/90으로 나누어 경기 당 이벤트 데이터로 변환하는 작업을 진행했다.
이 과정에서 무한대 값이 발생하는 것을 방지하기 위해 0을 NaN으로 바꾸었고, 변환을 위한 연산 이후 다시 0으로 대체하였다.

# 데이터 스케일링
scaled_df = merged_df.copy()
scaled_columns = sc.fit_transform(merged_df[columns_to_normalize])
scaled_columns_df = pd.DataFrame(scaled_columns, columns=columns_to_normalize)
scaled_df[columns_to_normalize] = scaled_columns_df

# 합쳐진 데이터 저장
merged_df.to_csv(output_preprocessed_file, index=False)
print(f"Data has been written to {output_preprocessed_file}")
scaled_df.to_csv(output_scaled_file, index=False)
print(f"Scaled data has been written to {output_scaled_file}")

# 두 가지 데이터 프레임으로 리턴
return merged_df, scaled_df

 

데이터를 스케일링 해주고 스케일링 대상 데이터 프레임과 스케일링된 데이터 프레임을 리턴해주었다.

 def preprocessing(round_number, scaling_method):
    # CSV 파일 경로
    input_data_file = f'data/{round_number}-round-data.csv'
    input_xg_file = f'data/{round_number}-round-xg.csv'
    output_preprocessed_file = f'data/preprocessed/{round_number}-round-preprocessed.csv'
    output_scaled_file = f'data/preprocessed/{round_number}-round-scaled.csv'

    # 스케일링 방법 선택
    if scaling_method == 'standard':
        sc = StandardScaler()
    elif scaling_method == 'minmax':
        sc = MinMaxScaler()
    else:
        ValueError('Unknown scaling method, Use "standard" or "minmax"')

    # 파일이 존재하는지 확인
    if not os.path.exists(input_data_file):
        print(f"Input data files for round {round_number} do not exist. Crawling data...")
        import crawler
        player_data = crawler.data_center(round_number=round_number)

    if not os.path.exists(input_xg_file):
        print(f"Input xG data files for round {round_number} do not exist. Crawling data...")
        import xG_crawler
        xG = xG_crawler.xg_crawler(round_number=round_number)

 

이렇게 작성된 코드는 round_number를 인자로 받는 모듈로 만들어 작업 파일에 해당 라운드 데이터가 존재 하지 않는 경우 앞서 만든 크롤러 모듈을 실행하도록 하였다.
또한, scaling_method로 향후 분석에 필요한 스케일러의 종류를 선택할 수 있게 하였다.

 

 

TODO:
Radar Chart에 주로 사용되는 Defensive ActionsPossessin Won Feature 만들기

 

 

전체 소스 코드는 여기서 확인하실 수 있습니다.

 

GitHub - jeongbeenson19/K-league-pipeline-project

Contribute to jeongbeenson19/K-league-pipeline-project development by creating an account on GitHub.

github.com