오늘 할 일: 끝내주게 숨쉬기
article thumbnail

데이터프레임 형태를 갖는 어떤 데이터가 있다고 합시다. 이 데이터의 한 칼럼은 하나의 값이 아니라 여러 개의 값을 갖고 있습니다. 각각의 값들이 하나의 칼럼이 되어 새로운 값을 갖도록 하려면 어떻게 해야 할까요?

한 강의를 듣다가 문득 궁금증이 생겼는데, 강사님께서 직접 찾아보라고 하셔서 ㅎ.. 알아보았습니다. 유용하게 쓰일 법한 테크닉인데, 구현하는 방법은 굉장히 단순해서 놀랐어요. 

 

데이터 준비

import os
import pandas as pd
import numpy as np
from tqdm import tqdm
path = '../data/movielens'
movies_df = pd.read_csv(os.path.join(path, 'movies.csv'), index_col='movieId', encoding='utf-8')
print(movies_df.shape)
movies_df.head()

총 9742개의 영화 정보가 저장되어 있는데요, 영어 제목으로 된 타이틀과 영화가 포함하는 장르를 칼럼으로 갖고 있습니다. 영화의 장르는 하나일 수도 있고, 2개 이상일 수도 있습니다. 장르를 여러 개 갖는 경우, |(bar)로 구분되어 있어서 이후에 처리할 때 bar를 구분자로 해서 분리하겠습니다.

 

total_count = len(movies_df.index)
total_genres = list(set([genre for sublist in list(map(lambda x: x.split('|'), movies_df['genres'])) for genre in sublist]))

print(f"전체 영화 수: {total_count}")
print(f"전체 장르 수: {len(total_genres)}")
print(f"장르: {total_genres}")
전체 영화 수: 9742
전체 장르 수: 20
장르: ['Adventure', 'Musical', 'Mystery', 'Western', 'Animation', 'Documentary', 'Sci-Fi', 'Children', '(no genres listed)', 'Horror', 'Film-Noir', 'Drama', 'Romance', 'Thriller', 'IMAX', 'Comedy', 'Fantasy', 'War', 'Action', 'Crime']

장르 종류는 총 20개이고, 어드벤쳐, 애니메이션, SF, 로맨스 등이 있네요. 

genre_count = dict.fromkeys(total_genres)

for each_genre_list in movies_df['genres']:
    for genre in each_genre_list.split('|'):
        if genre_count[genre] == None:
            genre_count[genre] = 1
        else:
            genre_count[genre] = genre_count[genre]+1
            
genre_count
{'Adventure': 1263,
 'Musical': 334,
 'Mystery': 573,
 'Western': 167,
 'Animation': 611,
 'Documentary': 440,
 'Sci-Fi': 980,
 'Children': 664,
 '(no genres listed)': 34,
 'Horror': 978,
 'Film-Noir': 87,
 'Drama': 4361,
 'Romance': 1596,
 'Thriller': 1894,
 'IMAX': 158,
 'Comedy': 3756,
 'Fantasy': 779,
 'War': 382,
 'Action': 1828,
 'Crime': 1199}

장르마다 속하는 영화가 몇 개가 있는지 세보았습니다. 드라마가 사천여개로 가장 많은 영화를 포함하고 있고, 장르가 없는 영화 (no genres listed)는 34개가 있습니다. 이제 이 딕셔너리를 이용해서 장르마다 열을 만들고, 영화가 해당 장르를 포함하면 그 열에 포함되는 영화 개수를 값으로 갖도록 데이터프레임을 만들어 보겠습니다.

 

방법 1: iterrows() 사용

영화별로 전체 장르 목록을 구성한 비어있는 데이터 프레임을 생성하고,  for 반복문을 통해 row를 돌면서 각 row의 값을 업데이트합니다.

%%time

genre_representation = pd.DataFrame(columns=sorted(total_genres), index=movies_df.index)
for index, each_row in tqdm(movies_df.iterrows()):
    dict_temp = {i: genre_count[i] for i in each_row['genres'].split('|')} # key는 장르, value는 장르별 가중치
    row_to_add = pd.DataFrame(dict_temp, index=[index])
    genre_representation.update(row_to_add) # 해당하는 인덱스의 값들을 업데이트함

genre_representation

만개가 되지 않는 행을 전부 도는데 1분 26초가 걸렸습니다. 좀 기네요..

 

방법 2: tolist()

%%time
def tag_map(value):
    dict_temp = {i: genre_count[i] for i in value.split('|')}
    return dict_temp
movies_df['map'] = movies_df['genres'].apply(lambda x: tag_map(x))

genre_map = pd.DataFrame(movies_df['map'].tolist(), index=movies_df.index)
genre_map

movies_df.loc[1].to_dict()
{'title': 'Toy Story (1995)',
 'genres': 'Adventure|Animation|Children|Comedy|Fantasy',
 'map': {'Adventure': 1263,
  'Animation': 611,
  'Children': 664,
  'Comedy': 3756,
  'Fantasy': 779}}

tag_map 함수를 이용해 장르마다 속한 영화 수를 맵핑하여 map이라는 새로운 칼럼을 생성합니다. 이 map 칼럼은 위와 같이 딕셔너리를 값으로 갖고 있어요. 이 칼럼에 tolist()를 적용하면 매우 빠르게 데이터프레임을 만들 수 있습니다. 115ms 만에 결과가 찍혔네요. 위에서 약 1분 30초가 걸렸던 것과는 확연한 차이를 보입니다.

 

방법 3: apply(pd.Series)

%%time
def tag_map(value):
    dict_temp = {i: genre_count[i] for i in value.split('|')}
    return dict_temp
movies_df['map'] = movies_df['genres'].apply(lambda x: tag_map(x))

genre_map = movies_df['map'].apply(pd.Series)
genre_map

apply에 시리즈 클래스를 적용하는 방법도 크게 다르지 않습니다. 약 5초가 걸렸는데, 방법 2보다는 조금 느리지만 iterrows보다는 역시 훨씬 빠르네요.

 

이상 데이터프레임 내 딕셔너리 값을 이용해 새로운 데이터프레임을 생성하는 방법을 알아보았습니다. 알기 전에는 막막했는데, 깨닫고 나니 생각보다 너무 단순해서 허무했네요ㅎㅎ 판다스에서는 이런 과정들을 일컬어 Vectorization(벡터화)라고 합니다. 판다스로 전처리를 할 때 속도가 너무 느려서 새로운 방법을 찾고 싶다! 한다면 pandas optimization, pandas vectorized method 등으로 서치해보시기를 추천합니다.