카카오 아레나 EDA 코드 정리

np.repeat, map 등과 관련하여

Posted by Minki on June 8, 2020

잘못된 내용은 언제든지 밑의 댓글로 알려주세요!

들어가기

카카오 아레나 멜론 플레이리스트 공모전을 준비하면서 모르는 코드를 정리한 글입니다. 추가적인 EDA는 이 링크를 클릭하여 보실 수 있습니다.

matplot과 관련한 폰트 매니저 라이브러리

매번 맥을 사용할때마다 폰트 출력 문제가 있었습니다. 다음과 같은 라이브러리를 사용하면 쉽게 폰트 출력 문제를 해결할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# font관련 import 설정
from pandas.plotting import register_matplotlib_converters
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
font_path = '/Users/minki/Library/Fonts/NanumGothic.ttf'
font_name = fm.FontProperties(fname=font_path, size=10).get_name()
plt.rc('font', family=font_name, size=12)
plt.rcParams["figure.figsize"] = (20, 10)
register_matplotlib_converters()
mpl.font_manager._rebuild()
mpl.pyplot.rc('font', family='NanumGothic')

python assign 함수

R을 사용하면서는 몇번 assign 함수를 사용해봤지만 파이썬에서는 처음으로 assign 함수를 사용해 봤습니다. assign은 말 그래도 데이터프레임이 변수를 할당하는 함수로 tensorflow에서도 tensor를 할당하기 위해 사용합니다.

1
2
3
4
5
6
# assign 함수 미활용
gnr_code['join_code'] = gnr_code['gnr_code'].str[0:4]

# assign 함수 활용
# 열의 이름이 assign 안에 할당됩니다.
gnr_code = gnr_code.assign(join_code = gnr_code['gnr_code'].str[0:4])

pd.read_json

말 그래도 json 함수를 불러오는데 사용합니다. 불러오는 타입에는 'frame''series'가 있는데 default는 'frame'입니다.

1
pd.read_json('file', typ = 'frame)

map 함수

map 함수는 파이썬에서 매우 유용하게 사용할 수 있는 함수입니다. 먼저 map은 이터레이터(iterator)를 객체로 받아 사용합니다. 여기서 iterator란 모든 반복가능한 객체를 의미합니다. iterator의 종류로는 다음과 같은것들이 있습니다.

iterator 종류

  • list
  • tuple
  • range 함수

어떤 객체가 iterator인지 분명하지 않다면 dir()을 사용하여 출력된 값 중 __iter__가 있는지 확인하면 됩니다.

map 함수 활용

일반적으로 map 함수는 반복을 줄이기 위해 많이 사용합니다. 예를들어 어떤 리스트의 모든 값을 정수로 만든다고 가정해봅시다. 그러면 map 함수의 유무에 따라 다음과 같이 코드가 작성될 수 있습니다.

1
2
3
4
5
6
7
8
9
ex_list = [1.5, 2.5, 3.5, 4.5, 5.5]

# map 함수를 사용하지 않았을 시
for i in range(ex_list):
    ex_list[i] = int(ex_list[i])

# map 함수를 사용했을 시
list(map(int, ex_list))

이렇게 map 함수를 사용한다면 코드를 간결하게 할 수 있을 뿐 아니라, 반복문을 줄여 러닝타임을 크게 줄일 수 있습니다. 또한 map함수는 출력을 map함수로 하기 때문에 꼭 list나 tuple, pd.DataFrame을 사용하여 출력값을 변경해주어야 합니다.

np.repeat 함수

이제 카카오 아레나에서 map 함수가 어떻게 사용됐는지 보도록 하겠습니다. 우선 코드가 사용된 전문을 살펴보겠습니다.

1
2
3
4
5
6
song_gnr_map_unnest = np.dstack(
    (
        np.repeat(song_gnr_map.id.values, list(map(len, song_gnr_map.song_gn_gnr_basket))), 
        np.concatenate(song_gnr_map.song_gn_gnr_basket.values)
    )
)

여기서 사용된 list(map(len, song_gnr_map.song_gn_gnr_basket))이 코드는 song_gnr_map함수의 song_gn_gnr_basket열의 모든 인자를 받아 그 인자의 길이를 출력해주는 형태입니다. 그렇다면 다음과 같이 출력됩니다.

index id song_gn_gnr_basket len
0 0 [GN0900] 1
14 14 [GN2700, GN1100] 2
30 30 [GN2500, GN1500, GN0200] 3

그럼 이후 코드도 살펴보겠습니다. 이후 코드에 사용된 np.repeatnp.repeat(a, b) a를 b만큼 반복해달라는 명령어입니다. 여기서 song_gnr_map의 id를 출력할 때, song_gnr_map.iloc[:, 1], song_grn_map['id']와 같이 출력하는 것이 아닌 song_gnr_map.id.values로 출력하는 이유는 뒤에 .values라는 명령어를 붙히면 출력되는 데이터의 타입을 numpy.ndarray로 만들어주기 때문입니다. 그리고 이는 np.repeat이 input으로 array를 받기 때문입니다.

np.concatenate 함수

이제 다음으로 np.concatenate함수를 보도록 하겠습니다. 사실 이 np.concatenate함수는 배열들을 합치는데 사용되는 함수입니다. 잠시 예제를 보고가겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy

a = np.array([[1, 2], [3, 4]]) # shape = (2, 2)
b = np.array([[7, 8], [9, 10], [11, 12]]) # shape = (3, 2)

c = np.concatenate((a, b), axis=0) # shape = (5, 2)

'''
c 결과

array([[ 1,  2],
       [ 3,  4],
       [ 7,  8],
       [ 9, 10],
       [11, 12]])
'''

그런데 np.concatenate의 axis 옵션을 None으로 주면 array의 차원이 무조건 (?, )형태로 나오게 됩니다. 그러니까 array 안의 list 배열이 완전히 분해되어 일렬로 나열된다는 뜻입니다. 실제로 적용해보면 다음과 같습니다.

1
2
3
4
5
6
7
c = np.concatenate((a, b), axis=None) # shape = (10, )

'''
c 결과

array([ 1,  2,  3,  4,  7,  8,  9, 10, 11, 12])
'''

이처럼 np.concatenate를 사용하여도 컬럼내 중복을 제거할 수 있습니다. 그렇다면 이제 드디어 eda 전처리 코드를 다 이해할 수 있습니다. 카카오 아레나 전처리 코드 논리 전개는 다음과 같습니다.

  1. 곡별 통계를 위해 song_gn_gnr_basket 컬럼 안의 중복 제거 필요
  2. np.repeat(song_gnr_map.id.values, list(map(len, song_gnr_map.song_gn_gnr_basket)))을 통해 id의 중복 제거. shape = (803918, )
  3. np.concatenate(song_gnr_map.song_gn_gnr_basket.values)를 통해 장르 중복 제거. shape = (803918, )
  4. np.dstack을 통해 위 두개 데이터를 좌우로 결합하여 최종 데이터셋 구축

groupby & nunique

nunique

이 함수는 중복을 제거한 값들만 numpy.ndarray로 출력해주는 함수입니다. 장르 데이터 프레임으로 확인해보면 다음과 같습니다.

1
2
len(gnr_code) # 30
len(gnr_code.gnr_name.nunique()) # 26(gnr_name의 중복 제거 후 출력)

groupby

groupby함수는 그룹별로 데이터프레임의 항목을 묶음 지어주는 함수입니다. 다음의 코드를 사용하면 정말 쉽게 곡별 장르의 개수를 파악할 수 있습니다. 여기서 특이했던 점은 reset_index의 name을 변경함으로써 gnr_code열의 이름을 변경할 수 있습니다.

1
2
3
4
5
song_gnr_count = song_gnr_map.groupby('song_id').gnr_code.nunique().reset_index(name = 'mapping_gnr_cnt')

#1. song_id별로 그룹
#2. 그룹별로 된 데이어프레임에서 gnr_code만 중복을 제거하여 추출
#3. gnr_code의 이름을 mapping_gnr_cnt로 변경

apply 함수

apply함수는 lambda 함수와 많이 사용되는 함수입니다. 사실 사용할 때마다 헷갈리는 부분이 있어 이번 기회로 정리해보려고 합니다. 우선 apply함수는 map함수와 공통점이 있습니다. 바로 반복 연산을 매우 빠르게 만들어준다는 것인데요, 여기서 차이점은 map은 주로 list와 사용된다면 apply함수는 DataFrame과 많이 사용됩니다. 그럼 지금부터 살펴보겠습니다.

apply 단독 활용

apply가 단독으로 활용되는 형태는 DataFrame.apply(function, axis = 0 or 1)입니다. 그럼 사용 예시를 살펴보겠습니다. 먼저 데이터프레임을 생성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
df = pd.DataFrame({'수학' : [50, 60, 70, 80], '영어' : [40, 45, 50, 55],
                  '과학' : [60, 65, 70, 80]}, index = ['갑', '을', '병', '정']) # shape = (4, 3)

'''
수학	영어	과학
갑	50	40	60
을	60	45	65
병	70	50	70
정	80	55	80
'''

이제 axis를 활용해 apply 함수를 사용해보겠습니다. 여기서 axis의 의미는 다음과 같습니다. 특이하게 axis로 지정한 행 또는 열이 기준으로 선택된다는 것이 아니라는 점에 주의하시면 사용하는데 큰 어려움은 없을 것이라 생각됩니다.

  • apply(fun, axis = 0) : 행 기준을 없애고, 열단위로 fun 적용
  • apply(fun, axis = 1) : 열 기준을 없애고, 행단위로 fun 적용
1
2
3
4
5
6
df.apply(np.mean, axis = 0) # shape(3, )
'''
수학    65.00
영어    47.50
과학    68.75
'''
1
2
3
4
5
6
7
df.apply(np.mean, axis = 1) # shape(4, )
'''
갑    50.000000
을    56.666667
병    63.333333
정    71.666667
'''

보시면 아시겠지만 axis = 0 에서는 열의 인덱스가 나오고, axis = 1에서는 행의 인덱스가 나옴을 알 수 있습니다.

lambda 활용

lambda는 런타임 시 생성해서 사용할 수 있는 익명함수입니다. 그렇기에 lmabda 함수는 런타임시 사용되고 바로 버려지는 함수라고 생각하시면 됩니다.

1. lambda 기본 활용

우선 lambda의 기본 형태는 다음과 같습니다. lambda x : f(x) 여기서 x는 lamda의 인풋값을 의미하고 f(x)는 출력값을 의미합니다. 예를들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
f1 = lambda(x : x**3)
f1(3)

'''
27
'''

f2 = lambda(x, y : x + y)
f2(3, 4)

'''
7
'''

2. lambda + map 활용

lambda와 map함수는 같이 사용되면 정말 강력한 성능을 발휘합니다. 그 이유는 map으로 하나씩 불러온 객체를 lambda 함수에 바로바로 넣어줄 수 있기 때문입니다. 거두절미하고 바로 사용 예시를 보도록 하겠습니다. 기본 사용 형태는 다음과 같습니다. map(lambda(x : fun(x))) 여기서 x에는 list나 tuple과 같은 반복 가능한 객체를 넣어주어야 한다는점도 다시한번 기억하시면 좋을것같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a_list = [1, 2, 3, 4, 5]
b_list = [1, 3, 5, 7, 9]

f3 = map(lambda x : x*2, a_list)
print(list(f3)) # [2, 4, 6, 8, 10]

for i in list(map(lambda x, y: x+ y, a_list, b_list)):
    print(i)

'''
2
5
8
11
14
'''

3. lambda + apply 활용

lambda 함수는 list나 tuple을 주 객체로 받는 map함수 뿐만 아니라 데이터프레임을 주 객체로 받는 apply함수와도 궁합이 잘 맞습니다. 기본 사용 형태는 다음과 같습니다. df.apply(lambda x : f(x)) 이번에도 바로 예시를 보겠습니다. df는 위에서 만든 df를 사용하겠습니다.

3-1. apply 함수 출력

먼저 apply 함수를 제대로 이해하기 위하여 apply 함수가 어떻게 출력되는지 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
df.apply(lambda x : print(x), axis = 0)

'''
갑    50
을    60
병    70
정    80
Name: 수학, dtype: int64
갑    40
을    45
병    50
정    55
Name: 영어, dtype: int64
갑    60
을    65
병    70
정    80
Name: 과학, dtype: int64
'''

위의 출력값에서 알 수 있듯이 axis = 0옵션에 의해 행 기준을 없애고 열 기준으로 출력하는 것을 알 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
df.apply(lambda x: print(x), axis = 1)

'''
수학    50
영어    40
과학    60
Name: 갑, dtype: int64
수학    60
영어    45
과학    65
Name: 을, dtype: int64
수학    70
영어    50
과학    70
Name: 병, dtype: int64
수학    80
영어    55
과학    80
Name: 정, dtype: int64
'''

반대로 axis = 1을 적용하면 열 기준을 없애고 행 기준으로 값을 출력함을 알 수 있습니다. 이렇게 하나하나씩 출력하기 때문에 apply 내에 바로바로 다양한 함수를 적용할 수 있습니다.

1
2
3
4
5
# 모든 값에 *2
df.apply(lambda x: x*2)

# 수학 값에만 *2
df['수학'].apply(lambda x: x*2)

3-2. apply 함수 + if

apply 함수는 if문을 축약할 때도 사용됩니다. 위의 df의 수학과 과학 점수를 가지고 해당 학생이 수학을 잘하는지 과학을 잘하는지 평가하기 위한 코드를 작성해봅시다. 이때 for문 대신에 apply lambda를 활용하면 매우 간단하게 코드를 짤 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 단순 for문 사용
df['평가'] = '평가'
for i in range(len(df)):
    if df['수학'][i] >= df['과학'][i]:
        df['평가'][i] = '수학을 잘함'
    else :
        df['평가'][i] = '과학을 잘함'

# iterrows() for문 사용
for i, row in df.iterrows():
    if df.at[i, '수학'] >= df.at[i, '과학']:
        df.at[i, '평가'] = '수학을 잘함'
    else:
        df.at[i, '평가'] = '과학을 잘함'

# apply, lambda 활용
df['평가'] = df.apply(lambda x : '수학을 잘함' if (x['수학'] >= x['과학']) else '과학을 잘함', axis = 1)

이처럼 apply 함수를 활용하면 for문을 사용하지 않고도 매우 간단하게 함수를 생성할 수 있습니다.

3-3. apply 함수 + slice

apply함수를 사용하면 slice index 역시 편하게 할 수 있습니다. 예시를 위해 위의 데이터셋의 평가 컬럼에서 앞의 두글자만 뽑아보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
df['우수과목'] = df['평가'].apply(lambda x: x[0:2])

#만약 슬라이싱 대상이 str 형이 아니라면 간단하게 str을 사용하면 됩니다.
df['수학앞자리'] = df['수학'].apply(lambda x: str(x)[0:1])

'''
	수학	영어	과학	평가	우수과목	수학앞자리
갑	50	40	60	과학을 잘함	과학	5
을	60	45	65	과학을 잘함	과학	6
병	70	50	70	수학을 잘함	수학	7
정	80	55	80	수학을 잘함	수학	
'''

이처럼 apply 함수를 잘 활용하면 데이터프레임 가공에 있어서 정말 많은 시간을 단축할 수 있습니다.


reference